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.
@@ -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.pre"
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
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: []