fastlane-plugin-saucectl 0.1.0.pre.beta.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
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
@@ -0,0 +1,46 @@
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
@@ -0,0 +1,201 @@
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
@@ -0,0 +1,168 @@
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 XCTest
12
+ include FileUtils
13
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
14
+ TEST_FUNCTION_REGEX = /(test+[A-Z][a-zA-Z]+)[(][)]/.freeze
15
+
16
+ def initialize(config)
17
+ @config = config
18
+ end
19
+
20
+ def test_plan_exists?
21
+ File.exist?(Dir["**/#{@config[:test_plan]}.xctestplan"][0])
22
+ rescue StandardError
23
+ false
24
+ end
25
+
26
+ def valid_test_plan?
27
+ File.exist?(Dir["**/#{@config[:test_plan]}.xctestplan"][0])
28
+ rescue StandardError
29
+ UI.user_error!("#{@config[:test_plan]} was not found in workspace")
30
+ end
31
+
32
+ def fetch_test_plan
33
+ plan_path = Dir["**/#{@config[:test_plan]}.xctestplan"][0]
34
+ selected = File.read(plan_path)
35
+ JSON.parse(selected)["testTargets"][0]
36
+ end
37
+
38
+ def fetch_target_from_test_plan
39
+ fetch_test_plan["target"]["name"]
40
+ end
41
+
42
+ def test_data
43
+ if test_plan_exists?
44
+ valid_test_plan?
45
+ if fetch_test_plan.include?("skippedTests")
46
+ strip_skipped(all_tests)
47
+ else
48
+ fetch_selected_tests
49
+ end
50
+ else
51
+ all_tests
52
+ end
53
+ end
54
+
55
+ def test_target
56
+ @config[:test_target].nil? ? fetch_target_from_test_plan : @config[:test_target]
57
+ end
58
+
59
+ def test_distribution
60
+ test_distribution_check
61
+ tests_arr = []
62
+
63
+ test_distribution = @config[:test_plan].nil? ? @config[:test_distribution] : 'testPlan'
64
+ case test_distribution
65
+ when 'testCase', 'testPlan'
66
+ test_data.each do |type|
67
+ type[:tests].each { |test| tests_arr << "#{test_target}.#{type[:class]}/#{test}" }
68
+ end
69
+ else
70
+ test_data.each do |type|
71
+ tests_arr << "#{test_target}.#{type[:class]}"
72
+ end
73
+ end
74
+ tests_arr.uniq
75
+ end
76
+
77
+ def test_distribution_check
78
+ return @config[:test_distribution] if @config[:test_distribution].kind_of?(Array)
79
+
80
+ distribution_types = %w[class testCase shard]
81
+ unless distribution_types.include?(@config[:test_distribution]) || @config[:test_distribution].nil?
82
+ UI.user_error!("#{@config[:test_distribution]} is not a valid method of test distribution. \n Supported types for iOS: \n #{distribution_types}")
83
+ end
84
+ end
85
+
86
+ def fetch_selected_tests
87
+ ui_tests = []
88
+ fetch_test_plan["selectedTests"].each do |test|
89
+ test_case = test.gsub('/', ' ').split
90
+ ui_tests << { class: test_case[0], tests: [test_case[1].gsub(/[()]/, "").to_s] }
91
+ end
92
+ ui_tests
93
+ end
94
+
95
+ def strip_skipped(all_tests)
96
+ enabled_ui_tests = []
97
+ skipped_tests = fetch_disabled_tests
98
+ all_tests.each do |tests|
99
+ tests[:tests].each do |test|
100
+ unless skipped_tests.to_s.include?(test)
101
+ enabled_ui_tests << { class: tests[:class].to_s, tests: [test] }
102
+ end
103
+ end
104
+ end
105
+ enabled_ui_tests
106
+ end
107
+
108
+ def fetch_disabled_tests
109
+ skipped = fetch_test_plan["skippedTests"]
110
+ ui_tests = []
111
+ skipped.each do |item|
112
+ if item.include?('/')
113
+ test_case = item.gsub('/', ' ').split
114
+ ui_tests << sort_ui_tests(test_case[0], test_case[1])
115
+ else
116
+ ui_tests << scan_test_class(item)
117
+ end
118
+ end
119
+ ui_tests
120
+ end
121
+
122
+ def sort_ui_tests(cls, test_case)
123
+ { class: cls, tests: test_case.gsub(/[()]/, "").to_s }
124
+ end
125
+
126
+ def all_tests
127
+ test_details = []
128
+ test_dir = Dir["**/#{test_target}"][0]
129
+ search_retrieve_test_classes(test_dir).each do |f|
130
+ next unless File.basename(f) =~ CLASS_NAME_REGEX
131
+
132
+ test_details << { class: File.basename(f).gsub(FILE_TYPE_REGEX, ""), tests: tests_from(f) }
133
+ end
134
+
135
+ strip_empty(test_details)
136
+ end
137
+
138
+ def scan_test_class(cls)
139
+ test_details = []
140
+ test_dir = Dir["**/#{test_target}"][0]
141
+ search_retrieve_test_classes(test_dir).each do |f|
142
+ next unless File.basename(f) =~ /#{cls}/
143
+
144
+ test_details << { class: File.basename(f).gsub(FILE_TYPE_REGEX, ""), tests: tests_from(f) }
145
+ end
146
+
147
+ strip_empty(test_details)
148
+ end
149
+
150
+ def tests_from(path)
151
+ stdout, = find(path, "func")
152
+ test_cases = []
153
+ stdout.split.each do |line|
154
+ test_cases << line.match(TEST_FUNCTION_REGEX).to_s.gsub(/[()]/, "") if line =~ TEST_FUNCTION_REGEX
155
+ end
156
+ test_cases
157
+ end
158
+
159
+ def strip_empty(test_details)
160
+ tests = []
161
+ test_details.each do |test|
162
+ tests << test unless test[:tests].size.zero?
163
+ end
164
+ tests
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,5 @@
1
+ module Fastlane
2
+ module Saucectl
3
+ VERSION = "0.1.0-beta.2"
4
+ end
5
+ end
@@ -0,0 +1,17 @@
1
+ require 'fastlane'
2
+
3
+ module Fastlane
4
+ module Saucectl
5
+ UI = FastlaneCore::UI
6
+ # Return all .rb files inside the "actions" and "helper" directory
7
+ def self.all_classes
8
+ Dir[File.expand_path('**/actions/*_action.rb', File.dirname(__FILE__))]
9
+ end
10
+ end
11
+ end
12
+
13
+ # By default we want to import all available actions and helpers
14
+ # A plugin can contain any number of actions and plugins
15
+ Fastlane::Saucectl.all_classes.each do |current|
16
+ require current
17
+ end
metadata ADDED
@@ -0,0 +1,216 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastlane-plugin-saucectl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre.beta.2
5
+ platform: ruby
6
+ authors:
7
+ - Ian Hamilton
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-03-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: fastlane
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ox
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 2.14.5
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 2.14.5
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec_junit_formatter
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '='
116
+ - !ruby/object:Gem::Version
117
+ version: 1.12.1
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '='
123
+ - !ruby/object:Gem::Version
124
+ version: 1.12.1
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-performance
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubocop-require_tools
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: webmock
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description:
168
+ email: ian.ross.hamilton@gmail.com
169
+ executables: []
170
+ extensions: []
171
+ extra_rdoc_files: []
172
+ files:
173
+ - lib/fastlane/plugin/saucectl.rb
174
+ - lib/fastlane/plugin/saucectl/actions/delete_from_storage_action.rb
175
+ - lib/fastlane/plugin/saucectl/actions/disabled_tests_action.rb
176
+ - lib/fastlane/plugin/saucectl/actions/install_toolkit_action.rb
177
+ - lib/fastlane/plugin/saucectl/actions/sauce_apps_action.rb
178
+ - lib/fastlane/plugin/saucectl/actions/sauce_config_action.rb
179
+ - lib/fastlane/plugin/saucectl/actions/sauce_devices_action.rb
180
+ - lib/fastlane/plugin/saucectl/actions/sauce_runner_action.rb
181
+ - lib/fastlane/plugin/saucectl/actions/sauce_upload_action.rb
182
+ - lib/fastlane/plugin/saucectl/helper/api.rb
183
+ - lib/fastlane/plugin/saucectl/helper/config.rb
184
+ - lib/fastlane/plugin/saucectl/helper/espresso.rb
185
+ - lib/fastlane/plugin/saucectl/helper/file_utils.rb
186
+ - lib/fastlane/plugin/saucectl/helper/installer.rb
187
+ - lib/fastlane/plugin/saucectl/helper/runner.rb
188
+ - lib/fastlane/plugin/saucectl/helper/storage.rb
189
+ - lib/fastlane/plugin/saucectl/helper/suites.rb
190
+ - lib/fastlane/plugin/saucectl/helper/xctest.rb
191
+ - lib/fastlane/plugin/saucectl/version.rb
192
+ homepage: https://github.com/ianrhamilton/fastlane-plugin-saucectl
193
+ licenses:
194
+ - MIT
195
+ metadata:
196
+ rubygems_mfa_required: 'true'
197
+ post_install_message:
198
+ rdoc_options: []
199
+ require_paths:
200
+ - lib
201
+ required_ruby_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '2.6'
206
+ required_rubygems_version: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - ">"
209
+ - !ruby/object:Gem::Version
210
+ version: 1.3.1
211
+ requirements: []
212
+ rubygems_version: 3.3.7
213
+ signing_key:
214
+ specification_version: 4
215
+ summary: Test your iOS and and Android apps at scale using Sauce Labs toolkit.
216
+ test_files: []