fastlane-plugin-saucectl 0.1.0.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 +7 -0
- data/lib/fastlane/plugin/saucectl/actions/delete_from_storage_action.rb +96 -0
- data/lib/fastlane/plugin/saucectl/actions/disabled_tests_action.rb +86 -0
- data/lib/fastlane/plugin/saucectl/actions/install_toolkit_action.rb +30 -0
- data/lib/fastlane/plugin/saucectl/actions/sauce_apps_action.rb +96 -0
- data/lib/fastlane/plugin/saucectl/actions/sauce_config_action.rb +312 -0
- data/lib/fastlane/plugin/saucectl/actions/sauce_devices_action.rb +95 -0
- data/lib/fastlane/plugin/saucectl/actions/sauce_runner_action.rb +52 -0
- data/lib/fastlane/plugin/saucectl/actions/sauce_upload_action.rb +133 -0
- data/lib/fastlane/plugin/saucectl/helper/api.rb +115 -0
- data/lib/fastlane/plugin/saucectl/helper/config.rb +97 -0
- data/lib/fastlane/plugin/saucectl/helper/espresso.rb +93 -0
- data/lib/fastlane/plugin/saucectl/helper/file_utils.rb +27 -0
- data/lib/fastlane/plugin/saucectl/helper/installer.rb +51 -0
- data/lib/fastlane/plugin/saucectl/helper/runner.rb +36 -0
- data/lib/fastlane/plugin/saucectl/helper/storage.rb +46 -0
- data/lib/fastlane/plugin/saucectl/helper/suites.rb +201 -0
- data/lib/fastlane/plugin/saucectl/helper/xctest.rb +168 -0
- data/lib/fastlane/plugin/saucectl/version.rb +5 -0
- data/lib/fastlane/plugin/saucectl.rb +17 -0
- metadata +216 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'fastlane/action'
|
2
|
+
require_relative '../helper/api'
|
3
|
+
|
4
|
+
module Fastlane
|
5
|
+
module Actions
|
6
|
+
class SauceDevicesAction < Action
|
7
|
+
@messages = YAML.load_file("#{__dir__}/../strings/messages.yml")
|
8
|
+
|
9
|
+
def self.run(params)
|
10
|
+
platform = params[:platform]
|
11
|
+
case platform
|
12
|
+
when 'android'
|
13
|
+
Fastlane::Saucectl::Api.new(params).fetch_android_devices
|
14
|
+
when 'ios'
|
15
|
+
Fastlane::Saucectl::Api.new(params).fetch_ios_devices
|
16
|
+
else
|
17
|
+
Fastlane::Saucectl::Api.new(params).available_devices
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.description
|
22
|
+
"Returns a list of Device IDs for all devices in the data center that are currently free for testing."
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.details
|
26
|
+
"Returns a list of Device IDs for all devices in the data center that are currently free for testing."
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.available_options
|
30
|
+
[
|
31
|
+
FastlaneCore::ConfigItem.new(key: :platform,
|
32
|
+
description: "Device platform that you wish to query",
|
33
|
+
optional: true,
|
34
|
+
is_string: true,
|
35
|
+
default_value: ''),
|
36
|
+
FastlaneCore::ConfigItem.new(key: :region,
|
37
|
+
description: "Data Center region (us or eu), set using: region: 'eu'",
|
38
|
+
optional: false,
|
39
|
+
type: String,
|
40
|
+
verify_block: proc do |value|
|
41
|
+
UI.user_error!(@messages['region_error'].gsub!('$region', value)) unless @messages['supported_regions'].include?(value)
|
42
|
+
end),
|
43
|
+
FastlaneCore::ConfigItem.new(key: :sauce_username,
|
44
|
+
env_name: "SAUCE_USERNAME",
|
45
|
+
description: "Your sauce labs username in order to authenticate upload requests",
|
46
|
+
default_value: Actions.lane_context[SharedValues::SAUCE_USERNAME],
|
47
|
+
optional: false,
|
48
|
+
type: String,
|
49
|
+
verify_block: proc do |value|
|
50
|
+
UI.user_error!(@messages['sauce_username_error']) unless value && !value.empty?
|
51
|
+
end),
|
52
|
+
FastlaneCore::ConfigItem.new(key: :sauce_access_key,
|
53
|
+
env_name: "SAUCE_ACCESS_KEY",
|
54
|
+
description: "Your sauce labs access key in order to authenticate upload requests",
|
55
|
+
default_value: Actions.lane_context[SharedValues::SAUCE_ACCESS_KEY],
|
56
|
+
optional: false,
|
57
|
+
type: String,
|
58
|
+
verify_block: proc do |value|
|
59
|
+
UI.user_error!(@messages['sauce_api_key_error']) unless value && !value.empty?
|
60
|
+
end)
|
61
|
+
]
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.authors
|
65
|
+
["Ian Hamilton"]
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.category
|
69
|
+
:testing
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.is_supported?(platform)
|
73
|
+
[:ios, :android].include?(platform)
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.example_code
|
77
|
+
[
|
78
|
+
"sauce_devices({platform: 'android',
|
79
|
+
region: 'eu',
|
80
|
+
sauce_username: 'foo',
|
81
|
+
sauce_access_key: 'bar123',
|
82
|
+
})",
|
83
|
+
"sauce_devices({region: 'eu',
|
84
|
+
sauce_username: 'foo',
|
85
|
+
sauce_access_key: 'bar123',
|
86
|
+
})",
|
87
|
+
"sauce_devices({region: 'us',
|
88
|
+
sauce_username: 'foo',
|
89
|
+
sauce_access_key: 'bar123',
|
90
|
+
})"
|
91
|
+
]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative '../helper/runner'
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
module Actions
|
5
|
+
class SauceRunnerAction < Action
|
6
|
+
@messages = YAML.load_file("#{__dir__}/../strings/messages.yml")
|
7
|
+
|
8
|
+
def self.run(run = '')
|
9
|
+
Saucectl::Runner.new.execute
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
"Execute automated tests on sauce labs platform via saucectl binary for specified configuration"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.available_options
|
17
|
+
[
|
18
|
+
FastlaneCore::ConfigItem.new(key: :sauce_username,
|
19
|
+
env_name: "SAUCE_USERNAME",
|
20
|
+
default_value: Actions.lane_context[SharedValues::SAUCE_USERNAME],
|
21
|
+
description: "Your sauce labs username",
|
22
|
+
optional: false,
|
23
|
+
is_string: true,
|
24
|
+
verify_block: proc do |value|
|
25
|
+
UI.user_error!(@messages['sauce_username_error']) if value.empty?
|
26
|
+
end),
|
27
|
+
FastlaneCore::ConfigItem.new(key: :sauce_access_key,
|
28
|
+
env_name: "SAUCE_ACCESS_KEY",
|
29
|
+
default_value: Actions.lane_context[SharedValues::SAUCE_ACCESS_KEY],
|
30
|
+
description: "Your sauce labs access key",
|
31
|
+
optional: false,
|
32
|
+
is_string: true,
|
33
|
+
verify_block: proc do |value|
|
34
|
+
UI.user_error!(@messages['sauce_api_key_error']) if value.empty?
|
35
|
+
end)
|
36
|
+
]
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.authors
|
40
|
+
["Ian Hamilton"]
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.category
|
44
|
+
:testing
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.is_supported?(platform)
|
48
|
+
[:ios, :android].include?(platform)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'fastlane/action'
|
2
|
+
require 'json'
|
3
|
+
require 'yaml'
|
4
|
+
require_relative '../helper/api'
|
5
|
+
|
6
|
+
module Fastlane
|
7
|
+
module Actions
|
8
|
+
module SharedValues
|
9
|
+
SAUCE_USERNAME = :SAUCE_USERNAME
|
10
|
+
SAUCE_ACCESS_KEY = :SAUCE_ACCESS_KEY
|
11
|
+
end
|
12
|
+
|
13
|
+
class SauceUploadAction < Action
|
14
|
+
@messages = YAML.load_file("#{__dir__}/../strings/messages.yml")
|
15
|
+
|
16
|
+
def self.run(params)
|
17
|
+
response = Fastlane::Saucectl::Api.new(params).upload
|
18
|
+
body = JSON.parse(response.body)
|
19
|
+
body['item']['id']
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.description
|
23
|
+
"Upload test artifacts to sauce labs storage"
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.details
|
27
|
+
"Upload test artifacts to sauce labs storage"
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.available_options
|
31
|
+
[
|
32
|
+
FastlaneCore::ConfigItem.new(key: :platform,
|
33
|
+
description: "application under test platform (ios or android)",
|
34
|
+
optional: false,
|
35
|
+
is_string: true,
|
36
|
+
verify_block: proc do |value|
|
37
|
+
UI.user_error!(@messages['platform_error']) if value.to_s.empty?
|
38
|
+
end),
|
39
|
+
FastlaneCore::ConfigItem.new(key: :file,
|
40
|
+
description: "File to upload to sauce storage",
|
41
|
+
optional: false,
|
42
|
+
is_string: true,
|
43
|
+
verify_block: proc do |value|
|
44
|
+
UI.user_error!(@messages['file_error']) unless value && !value.empty?
|
45
|
+
if value
|
46
|
+
UI.user_error!("Could not find file to upload \"#{value}\" ") unless File.exist?(value)
|
47
|
+
extname = File.extname(value)
|
48
|
+
UI.user_error!("Extension not supported for \"#{value}\" ") unless @messages['accepted_file_types'].include?(extname)
|
49
|
+
end
|
50
|
+
end),
|
51
|
+
FastlaneCore::ConfigItem.new(key: :app,
|
52
|
+
description: "Name of the application to be uploaded",
|
53
|
+
optional: false,
|
54
|
+
is_string: true,
|
55
|
+
verify_block: proc do |value|
|
56
|
+
UI.user_error!(@messages['app_name_error']) unless value && !value.empty?
|
57
|
+
end),
|
58
|
+
FastlaneCore::ConfigItem.new(key: :region,
|
59
|
+
description: "Data Center region (us or eu), set using: region: 'eu'",
|
60
|
+
optional: false,
|
61
|
+
is_string: true,
|
62
|
+
verify_block: proc do |value|
|
63
|
+
UI.user_error!(@messages['region_error'].gsub!('$region', value)) unless @messages['supported_regions'].include?(value)
|
64
|
+
end),
|
65
|
+
FastlaneCore::ConfigItem.new(key: :sauce_username,
|
66
|
+
env_name: "SAUCE_USERNAME",
|
67
|
+
description: "Your sauce labs username in order to authenticate upload requests",
|
68
|
+
default_value: Actions.lane_context[SharedValues::SAUCE_USERNAME],
|
69
|
+
optional: false,
|
70
|
+
type: String,
|
71
|
+
verify_block: proc do |value|
|
72
|
+
UI.user_error!(@messages['sauce_username_error']) unless value && !value.empty?
|
73
|
+
end),
|
74
|
+
FastlaneCore::ConfigItem.new(key: :sauce_access_key,
|
75
|
+
env_name: "SAUCE_ACCESS_KEY",
|
76
|
+
description: "Your sauce labs access key in order to authenticate upload requests",
|
77
|
+
default_value: Actions.lane_context[SharedValues::SAUCE_ACCESS_KEY],
|
78
|
+
optional: false,
|
79
|
+
type: String,
|
80
|
+
verify_block: proc do |value|
|
81
|
+
UI.user_error!(@messages['sauce_api_key_error']) unless value && !value.empty?
|
82
|
+
end)
|
83
|
+
]
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.authors
|
87
|
+
["Ian Hamilton"]
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.category
|
91
|
+
:testing
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.is_supported?(platform)
|
95
|
+
[:ios, :android].include?(platform)
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.example_code
|
99
|
+
[
|
100
|
+
"sauce_upload({
|
101
|
+
platform: 'android',
|
102
|
+
sauce_username: 'username',
|
103
|
+
sauce_access_key: 'accessKey',
|
104
|
+
app: 'Android.MyCustomApp.apk',
|
105
|
+
file: 'app/build/outputs/apk/debug/app-debug.apk',
|
106
|
+
region: 'eu'
|
107
|
+
})",
|
108
|
+
"sauce_upload({
|
109
|
+
platform: 'android',
|
110
|
+
sauce_username: 'username',
|
111
|
+
sauce_access_key: 'accessKey',
|
112
|
+
app: 'Android.MyCustomApp.apk',
|
113
|
+
file: 'app/build/outputs/apk/debug/app-debug.apk',
|
114
|
+
region: 'eu',
|
115
|
+
app_description: 'this is a test description'
|
116
|
+
})",
|
117
|
+
"sauce_upload({
|
118
|
+
platform: 'ios',
|
119
|
+
sauce_username: 'username',
|
120
|
+
sauce_access_key: 'accessKey',
|
121
|
+
app: 'MyTestApp.ipa',
|
122
|
+
file: 'path/to/my/app/MyTestApp.ipa',
|
123
|
+
region: 'eu'
|
124
|
+
})"
|
125
|
+
]
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.return_value
|
129
|
+
"Returns the application id of the app uploaded"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,115 @@
|
|
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
|
@@ -0,0 +1,97 @@
|
|
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
|
@@ -0,0 +1,93 @@
|
|
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
|
@@ -0,0 +1,27 @@
|
|
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
|
@@ -0,0 +1,51 @@
|
|
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
|