fastlane-plugin-saucectl 0.1.3.pre → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,166 @@
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 for ios applications based on user specified configuration properties
11
+ #
12
+ class IosSuites
13
+ include FileUtils
14
+
15
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
16
+
17
+ def initialize(config)
18
+ UI.user_error!("❌ For the ios platform you must specify `devices` array") if config[:devices].nil?
19
+ UI.user_error!("❌ #{config[:kind]} is not a supported test framework for iOS. Use xcuitest") unless config[:kind].eql?('xcuitest')
20
+ @config = config
21
+ end
22
+
23
+ def generate
24
+ check_for_android_params
25
+
26
+ if @config[:test_distribution] || @config[:test_plan]
27
+ create_test_distribution_suite
28
+ elsif @config[:test_class].kind_of?(Array)
29
+ custom_test_class_suite
30
+ else
31
+ test_runner_suite
32
+ end
33
+ end
34
+
35
+ def check_for_android_params
36
+ type = @config[:annotation] ? 'annotation' : 'size'
37
+ UI.user_error!("❌ execution by #{type} is not supported on the iOS platform!") if @config[:size] || @config[:annotation]
38
+ end
39
+
40
+ def create_test_distribution_suite
41
+ if @config[:test_distribution].eql?('shard')
42
+ shard_real_device_suites
43
+ else
44
+ test_distribution_suite
45
+ end
46
+ end
47
+
48
+ def test_distribution_array
49
+ @config[:test_class] || Fastlane::Saucectl::XCTest.new(@config).test_distribution
50
+ end
51
+
52
+ def suite_name(name)
53
+ if ENV['JOB_NAME'].nil? && ENV['BUILD_NUMBER'].nil?
54
+ "#{@config[:kind]}-#{name}"
55
+ else
56
+ "#{ENV['JOB_NAME']}-#{ENV['BUILD_NUMBER']}-#{name}"
57
+ end
58
+ end
59
+
60
+ def shard_real_device_suites
61
+ test_suites = []
62
+ arr = test_distribution_array
63
+ shards = arr.each_slice((arr.size / @config[:devices].size.to_f).round).to_a
64
+ shards.each_with_index do |suite, i|
65
+ device_name = @config[:devices][i].key?(:id) ? @config[:devices][i][:id] : @config[:devices][i][:name]
66
+ test_suites << {
67
+ 'name' => suite_name("#{device_name}-shard-#{i + 1}").downcase,
68
+ 'testOptions' => default_test_options(suite)
69
+ }.merge(real_device_options(@config[:devices][i]))
70
+ end
71
+ test_suites
72
+ end
73
+
74
+ def test_runner_suite
75
+ test_suites = []
76
+ @config[:devices].each do |device|
77
+ device_name = device.key?(:id) ? device[:id] : device[:name]
78
+ test_suites << {
79
+ 'name' => suite_name(device_name),
80
+ 'testOptions' => default_test_options(nil)
81
+ }.merge(real_device_options(device))
82
+ end
83
+ test_suites
84
+ end
85
+
86
+ def custom_test_class_suite
87
+ test_suites = []
88
+ @config[:devices].each do |device|
89
+ device_options = real_device_options(device)
90
+ test_classes = @config[:test_class].reject(&:empty?).join(',')
91
+ test_suites << {
92
+ 'name' => suite_name(device[:name]).downcase,
93
+ 'testOptions' => default_test_options(test_classes.split(','))
94
+ }.merge(device_options)
95
+ end
96
+ test_suites
97
+ end
98
+
99
+ def test_distribution_suite
100
+ test_suites = []
101
+ if @config[:test_plan]
102
+ @config[:devices].each do |device|
103
+ test_suites << {
104
+ 'name' => suite_name(@config[:test_plan]).downcase,
105
+ 'testOptions' => default_test_options(test_distribution_array)
106
+ }.merge(real_device_options(device))
107
+ end
108
+ else
109
+ @config[:devices].each do |device|
110
+ test_distribution_array.each do |test_type|
111
+ test_suites << {
112
+ 'name' => suite_name(test_type).downcase,
113
+ 'testOptions' => default_test_options(test_type)
114
+ }.merge(real_device_options(device))
115
+ end
116
+ end
117
+ end
118
+ test_suites
119
+ end
120
+
121
+ def test_plan_distribution
122
+ test_suites = []
123
+ @config[:devices].each do |device|
124
+ test_suites << {
125
+ 'name' => suite_name(@config[:test_plan]).downcase,
126
+ 'testOptions' => default_test_options(test_distribution_array)
127
+ }.merge(real_device_options(device))
128
+ end
129
+ test_suites
130
+ end
131
+
132
+ def real_device_options(device)
133
+ { 'devices' => [rdc_options(device)] }
134
+ end
135
+
136
+ def rdc_options(device)
137
+ device_type_key = device.key?(:id) ? 'id' : 'name'
138
+ name = device.key?(:id) ? device[:id] : device[:name]
139
+ base_device_hash = {
140
+ device_type_key => name,
141
+ 'orientation' => device[:orientation]
142
+ }.merge('options' => device_options(device))
143
+
144
+ unless device[:platform_version].nil?
145
+ base_device_hash = base_device_hash.merge({ 'platformVersion' => device[:platform_version] })
146
+ end
147
+
148
+ base_device_hash
149
+ end
150
+
151
+ def device_options(device)
152
+ {
153
+ 'carrierConnectivity' => device[:carrier_connectivity],
154
+ 'deviceType' => device[:device_type].upcase!,
155
+ 'private' => device[:private]
156
+ }
157
+ end
158
+
159
+ def default_test_options(test_type)
160
+ unless test_type.nil?
161
+ { 'class' => test_type }
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,39 @@
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
@@ -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,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.tr(" ", "_")}.#{type[:class]}/#{test}" }
68
+ end
69
+ else
70
+ test_data.each do |type|
71
+ tests_arr << "#{test_target.tr(" ", "_")}.#{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,12 @@
1
+ ---
2
+ platform_error: "No platform specified, set using: platform: 'android'"
3
+ app_path_error: "No App path given, set using: app_path: 'path/to/my/testApp.apk'"
4
+ file_error: "No file path given, set using: app_path: 'path/to/my/testApp.apk'"
5
+ app_name_error: "No App name given, set using: app_name: 'testApp.apk'"
6
+ test_runner_app_error: "No Test runner application given, set using: test_runner_app: 'testRunnerApp.apk'"
7
+ region_error: "$region is an invalid region. Supported regions are 'us' and 'eu'"
8
+ sauce_username_error: "No sauce labs username provided, set using: sauce_username: 'sauce user name', or consider setting your credentials as environment variables."
9
+ sauce_api_key_error: "No sauce labs access key provided, set using: sauce_access_key: '1234' or consider setting your credentials as environment variables."
10
+ supported_regions: ['us', 'eu']
11
+ accepted_file_types: ['.apk', '.aab', '.ipa', '.zip']
12
+ missing_file_name: "Please specify the name of the app that you wish to query on sauce storage"
@@ -0,0 +1,5 @@
1
+ module Fastlane
2
+ module Saucectl
3
+ VERSION = '0.1.3'.freeze
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ require 'fastlane/plugin/saucectl/version'
2
+
3
+ module Fastlane
4
+ module Saucectl
5
+ def self.all_classes
6
+ Dir[File.expand_path('*/{actions,helper}/*.rb', File.dirname(__FILE__))]
7
+ end
8
+ end
9
+ end
10
+
11
+ # By default we want to import all available actions and helpers
12
+ # A plugin can contain any number of actions and plugins
13
+ Fastlane::Saucectl.all_classes.each do |current|
14
+ require current
15
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-saucectl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3.pre
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ian Hamilton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-23 00:00:00.000000000 Z
11
+ date: 2022-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -169,7 +169,30 @@ email: ian.ross.hamilton@gmail.com
169
169
  executables: []
170
170
  extensions: []
171
171
  extra_rdoc_files: []
172
- files: []
172
+ files:
173
+ - LICENSE
174
+ - README.md
175
+ - lib/fastlane/plugin/saucectl.rb
176
+ - lib/fastlane/plugin/saucectl/actions/delete_from_storage_action.rb
177
+ - lib/fastlane/plugin/saucectl/actions/disabled_tests_action.rb
178
+ - lib/fastlane/plugin/saucectl/actions/install_saucectl_action.rb
179
+ - lib/fastlane/plugin/saucectl/actions/sauce_apps_action.rb
180
+ - lib/fastlane/plugin/saucectl/actions/sauce_config_action.rb
181
+ - lib/fastlane/plugin/saucectl/actions/sauce_devices_action.rb
182
+ - lib/fastlane/plugin/saucectl/actions/sauce_runner_action.rb
183
+ - lib/fastlane/plugin/saucectl/actions/sauce_upload_action.rb
184
+ - lib/fastlane/plugin/saucectl/helper/android_suites.rb
185
+ - lib/fastlane/plugin/saucectl/helper/api.rb
186
+ - lib/fastlane/plugin/saucectl/helper/config.rb
187
+ - lib/fastlane/plugin/saucectl/helper/espresso.rb
188
+ - lib/fastlane/plugin/saucectl/helper/file_utils.rb
189
+ - lib/fastlane/plugin/saucectl/helper/installer.rb
190
+ - lib/fastlane/plugin/saucectl/helper/ios_suites.rb
191
+ - lib/fastlane/plugin/saucectl/helper/runner.rb
192
+ - lib/fastlane/plugin/saucectl/helper/storage.rb
193
+ - lib/fastlane/plugin/saucectl/helper/xctest.rb
194
+ - lib/fastlane/plugin/saucectl/strings/messages.yml
195
+ - lib/fastlane/plugin/saucectl/version.rb
173
196
  homepage: https://github.com/ianrhamilton/fastlane-plugin-saucectl
174
197
  licenses:
175
198
  - MIT
@@ -183,15 +206,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
183
206
  requirements:
184
207
  - - ">="
185
208
  - !ruby/object:Gem::Version
186
- version: '2.6'
209
+ version: '2.5'
187
210
  required_rubygems_version: !ruby/object:Gem::Requirement
188
211
  requirements:
189
- - - ">"
212
+ - - ">="
190
213
  - !ruby/object:Gem::Version
191
- version: 1.3.1
214
+ version: '0'
192
215
  requirements: []
193
- rubygems_version: 3.3.7
216
+ rubygems_version: 3.3.10
194
217
  signing_key:
195
218
  specification_version: 4
196
- summary: Test your iOS and and Android apps at scale using Sauce Labs toolkit.
219
+ summary: Simplify the set up, configuration, upload, and execution of espresso and
220
+ XCUITest on the Sauce Labs platform by utilizing fastlane which will enable you
221
+ to test your iOS and Android apps at scale.
197
222
  test_files: []