fastlane-plugin-saucectl 0.1.2 → 0.1.3.pre
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.
- checksums.yaml +4 -4
- metadata +8 -32
- data/LICENSE +0 -21
- data/README.md +0 -81
- data/lib/fastlane/plugin/saucectl/actions/delete_from_storage_action.rb +0 -96
- data/lib/fastlane/plugin/saucectl/actions/disabled_tests_action.rb +0 -90
- data/lib/fastlane/plugin/saucectl/actions/install_saucectl_action.rb +0 -44
- data/lib/fastlane/plugin/saucectl/actions/sauce_apps_action.rb +0 -96
- data/lib/fastlane/plugin/saucectl/actions/sauce_config_action.rb +0 -320
- data/lib/fastlane/plugin/saucectl/actions/sauce_devices_action.rb +0 -95
- data/lib/fastlane/plugin/saucectl/actions/sauce_runner_action.rb +0 -56
- data/lib/fastlane/plugin/saucectl/actions/sauce_upload_action.rb +0 -133
- data/lib/fastlane/plugin/saucectl/helper/api.rb +0 -115
- data/lib/fastlane/plugin/saucectl/helper/config.rb +0 -97
- data/lib/fastlane/plugin/saucectl/helper/espresso.rb +0 -93
- data/lib/fastlane/plugin/saucectl/helper/file_utils.rb +0 -27
- data/lib/fastlane/plugin/saucectl/helper/installer.rb +0 -54
- data/lib/fastlane/plugin/saucectl/helper/runner.rb +0 -39
- data/lib/fastlane/plugin/saucectl/helper/storage.rb +0 -46
- data/lib/fastlane/plugin/saucectl/helper/suites.rb +0 -219
- data/lib/fastlane/plugin/saucectl/helper/xctest.rb +0 -168
- data/lib/fastlane/plugin/saucectl/strings/messages.yml +0 -12
- data/lib/fastlane/plugin/saucectl/version.rb +0 -5
- data/lib/fastlane/plugin/saucectl.rb +0 -15
|
@@ -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 android 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,54 +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(version)
|
|
16
|
-
timeout_in_seconds = 90
|
|
17
|
-
Timeout.timeout(timeout_in_seconds) do
|
|
18
|
-
download_saucectl_installer
|
|
19
|
-
execute_saucectl_installer(version)
|
|
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', ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE).read
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def execute_saucectl_installer(version)
|
|
34
|
-
saucectl_version = version[:version].nil? ? '' : "v#{version[:version]}"
|
|
35
|
-
status = system("sh sauce #{saucectl_version}")
|
|
36
|
-
status == 1 ? UI.user_error!("❌ failed to install saucectl") : status
|
|
37
|
-
executable = 'saucectl'
|
|
38
|
-
FileUtils.mv("bin/#{executable}", executable) unless File.exist?(executable)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def system(*cmd)
|
|
42
|
-
Open3.popen2e(*cmd) do |stdin, stdout_stderr, wait_thread|
|
|
43
|
-
Thread.new do
|
|
44
|
-
stdout_stderr.each do |out|
|
|
45
|
-
UI.message(out)
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
stdin.close
|
|
49
|
-
wait_thread.value
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,39 +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 do |out|
|
|
29
|
-
message = out.gsub(/(?:\[[^\]].*\])|(?:\(\d{4}\))/, '')
|
|
30
|
-
puts(message)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
stdin.close
|
|
34
|
-
wait_thread.value
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
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,219 +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
|
-
default_execution_suite
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def shard_virtual_device_suites
|
|
68
|
-
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)
|
|
69
|
-
test_suites = []
|
|
70
|
-
arr = test_distribution_array
|
|
71
|
-
shards = arr.each_slice((arr.size / @config[:emulators].size.to_f).round).to_a
|
|
72
|
-
shards.each_with_index do |suite, i|
|
|
73
|
-
test_suites << {
|
|
74
|
-
'name' => suite_name("shard #{i + 1}").downcase,
|
|
75
|
-
'testOptions' => default_test_options(suite)
|
|
76
|
-
}.merge(virtual_device_options(@config[:emulators][i]))
|
|
77
|
-
end
|
|
78
|
-
test_suites
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def shard_real_device_suites
|
|
82
|
-
test_suites = []
|
|
83
|
-
arr = test_distribution_array
|
|
84
|
-
shards = arr.each_slice((arr.size / @config[:devices].size.to_f).round).to_a
|
|
85
|
-
shards.each_with_index do |suite, i|
|
|
86
|
-
test_suites << {
|
|
87
|
-
'name' => suite_name("shard #{i + 1}").downcase,
|
|
88
|
-
'testOptions' => default_test_options(suite)
|
|
89
|
-
}.merge(real_device_options(@config[:devices][i]))
|
|
90
|
-
end
|
|
91
|
-
test_suites
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def test_plan_suites
|
|
95
|
-
test_suites = []
|
|
96
|
-
@config[:devices].each do |device|
|
|
97
|
-
test_suites << {
|
|
98
|
-
'name' => suite_name(@config[:test_plan].to_s).downcase,
|
|
99
|
-
'testOptions' => default_test_options(test_distribution_array)
|
|
100
|
-
}.merge(real_device_options(device))
|
|
101
|
-
end
|
|
102
|
-
test_suites
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def custom_test_classes
|
|
106
|
-
test_suites = []
|
|
107
|
-
devices = @config[:devices].nil? ? @config[:emulators] : @config[:devices]
|
|
108
|
-
devices.each do |device|
|
|
109
|
-
device_options = @config[:devices].nil? ? virtual_device_options(device) : real_device_options(device)
|
|
110
|
-
test_classes = @config[:test_class].reject(&:empty?).join(',')
|
|
111
|
-
test_suites << {
|
|
112
|
-
'name' => suite_name(device[:name]).downcase,
|
|
113
|
-
'testOptions' => default_test_options(test_classes.split(','))
|
|
114
|
-
}.merge(device_options)
|
|
115
|
-
end
|
|
116
|
-
test_suites
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def create_real_device_suites
|
|
120
|
-
if !@config[:test_plan].nil? && @config[:test_distribution].eql?('class')
|
|
121
|
-
test_plan_suites
|
|
122
|
-
elsif @config[:test_distribution] == 'shard'
|
|
123
|
-
shard_real_device_suites
|
|
124
|
-
elsif @config[:test_class].kind_of?(Array)
|
|
125
|
-
custom_test_classes
|
|
126
|
-
else
|
|
127
|
-
default_execution_suite
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def default_execution_suite
|
|
132
|
-
type = @config[:annotation] ? 'annotation' : 'size'
|
|
133
|
-
UI.user_error!("❌ execution by #{type} is not supported on the iOS platform!") if @config[:platform].eql?('ios') && (@config[:size] || @config[:annotation])
|
|
134
|
-
is_real_device = @config[:devices]
|
|
135
|
-
test_devices = @config[:devices] || @config[:emulators]
|
|
136
|
-
test_suites = []
|
|
137
|
-
if @config[:size] || @config[:annotation]
|
|
138
|
-
test_devices.each do |device|
|
|
139
|
-
test_suites << {
|
|
140
|
-
'name' => suite_name(@config[:size] || @config[:annotation]).downcase,
|
|
141
|
-
'testOptions' => default_test_options(@config[:size])
|
|
142
|
-
}.merge(is_real_device ? real_device_options(device) : virtual_device_options(device))
|
|
143
|
-
end
|
|
144
|
-
else
|
|
145
|
-
test_devices.each do |device|
|
|
146
|
-
test_distribution_array.each do |test_type|
|
|
147
|
-
test_suites << {
|
|
148
|
-
'name' => suite_name(test_type).downcase,
|
|
149
|
-
'testOptions' => default_test_options(test_type)
|
|
150
|
-
}.merge(is_real_device ? real_device_options(device) : virtual_device_options(device))
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
test_suites
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def virtual_device_options(device)
|
|
158
|
-
platform_versions = device[:platform_versions].reject(&:empty?).join(',')
|
|
159
|
-
{ 'emulators' => [{ 'name' => device[:name],
|
|
160
|
-
'orientation' => device[:orientation],
|
|
161
|
-
'platformVersions' => platform_versions.split(',') }] }
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def real_device_options(device)
|
|
165
|
-
{ 'devices' => [rdc_options(device)] }
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def rdc_options(device)
|
|
169
|
-
device_type_key = device.key?(:id) ? 'id' : 'name'
|
|
170
|
-
name = device.key?(:id) ? device[:id] : device[:name]
|
|
171
|
-
|
|
172
|
-
base_device_hash = {
|
|
173
|
-
device_type_key => name,
|
|
174
|
-
'orientation' => device[:orientation]
|
|
175
|
-
}.merge('options' => device_options(device))
|
|
176
|
-
|
|
177
|
-
unless device[:platform_version].nil?
|
|
178
|
-
base_device_hash = base_device_hash.merge({ 'platformVersion' => device[:platform_version] })
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
base_device_hash
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def device_options(device)
|
|
185
|
-
{
|
|
186
|
-
'carrierConnectivity' => device[:carrier_connectivity],
|
|
187
|
-
'deviceType' => device[:device_type].upcase!,
|
|
188
|
-
'private' => device[:private]
|
|
189
|
-
}
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def default_test_options(test_type)
|
|
193
|
-
if @config[:platform] == 'android'
|
|
194
|
-
test_option_type(test_type)
|
|
195
|
-
else
|
|
196
|
-
{ 'class' => test_type }
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def test_option_type(test_type)
|
|
201
|
-
if @config[:size] || @config[:annotation]
|
|
202
|
-
key = @config[:size] ? 'size' : 'annotation'
|
|
203
|
-
value = @config[:size] || @config[:annotation]
|
|
204
|
-
{ key => value }.merge(android_test_options)
|
|
205
|
-
else
|
|
206
|
-
test_option_type = @config[:test_distribution].eql?('package') ? 'package' : 'class'
|
|
207
|
-
{ test_option_type => test_type }.merge(android_test_options)
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def android_test_options
|
|
212
|
-
{
|
|
213
|
-
'clearPackageData' => @config[:clear_data],
|
|
214
|
-
'useTestOrchestrator' => @config[:use_test_orchestrator]
|
|
215
|
-
}
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
end
|