fastlane-plugin-saucectl 0.1.5 → 0.1.6.pre

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 637b1e3c3ccf3d188af05d598d348dff048e3877d5ac2ae1a16cf1829c299a3b
4
- data.tar.gz: 4db08970b9bd2638bae2aabfda7eddbc753e0820fcbebdf4357676affb172f1b
3
+ metadata.gz: ef06de19364379f2f875cef720dc633aa7183766a4d471274f53d660c17920c8
4
+ data.tar.gz: 5908aea97ba3ae4b2ef539ebd4d772c1d90f65ef0aa7978c39416263c6bb17ec
5
5
  SHA512:
6
- metadata.gz: da201db10c50520c11c47eaf262c6e31cc7d62deb51d3e66e0abd740fb691f46b9ab28d8cc2afc2e480621d537403fef298f2f1080398054a7bbabc8bb070c1b
7
- data.tar.gz: 556f168359742f5243d45a98af0ee7c4770172f7df174a17e43ba7824a3b8b0978ce26978f47c558e9f4afd65aa2cb5f749e40b1224a83907aa5deb912689fb7
6
+ metadata.gz: dda5e7f653297d58c86e3d1fdec85d646dbdc0524e4c97e690195274d58fcc8ad11897d9f6f6865b6804820612a3ba2b5c986616c673768d33a9a1ea7e1d44df
7
+ data.tar.gz: fbc36e7c6358194523241bb7c2373c15516fd521d528fb4992b9fdef3c1402d21221e5b1185de1708e212fa08e4af823da07302e143bc014525d07db1aca7525
data/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # fastlane-plugin-saucectl
2
2
 
3
3
  [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-saucectl)
4
- [![Gem Version](https://badge.fury.io/rb/fastlane-plugin-saucectl.svg)](https://badge.fury.io/rb/fastlane-plugin-saucectl)
5
- [![Gem Downloads](https://img.shields.io/gem/dt/fastlane-plugin-saucectl?color=light-green)](https://img.shields.io/gem/dt/fastlane-plugin-saucectl)
4
+ [![codecov](https://codecov.io/gh/ianrhamilton/fastlane-plugin-rsaucectl/branch/main/graph/badge.svg?token=NSVhqgYFYv)](https://codecov.io/gh/ianrhamilton/fastlane-plugin-saucectl)
6
5
 
7
6
  ## Getting Started
8
7
 
@@ -14,24 +13,31 @@ fastlane add_plugin saucectl
14
13
 
15
14
  ## About fastlane-plugin-saucectl
16
15
 
17
- The purpose of this plugin is to simplify the set-up, configuration, upload, and execution of espresso and XCUITest on the Sauce Labs platform by utilizing fastlane which will enable you to test your iOS and Android apps at scale.
16
+ The purpose of this plugin is to simplify the set up, configuration, upload, and execution of espresso and XCUITest on the Sauce Labs platform by utilizing fastlane which will enable you to test your iOS and Android apps at scale.
18
17
 
19
- **IMPORTANT:** To use this plugin to execute UI tests on Sauce Labs, your test class names must proceed with `Spec`, `Specs`, `Tests`, or `Test`, for example `ExampleSpec`, `ExampleSpecs`, `ExampleTest`, or `ExampleTests`. Your test case names must also begin with `test`, for example `testIDoSomething`, `testIDoSomethingElse`. This is so that the plugin can search for test classes and their included test cases.
18
+ **IMPORTANT:** in order for you to use this plugin to execute UI tests, your test class names must proceed with Spec, Specs, Tests, or Test, for example ExampleSpec, ExampleSpecs, ExampleTest, ExampleTests. Your test case names must also begin with test, for example testIDoSomething, testIDoSomethingElse. This is so that the the plugin can search for test classes and their included test cases.
20
19
 
21
20
  Failure to do this will result in missing test classes and test cases from your test run.
22
21
 
23
22
  **For a detailed introduction to each of the actions available within this plugin, please see the [documentation](https://ianrhamilton.github.io/fastlane-plugin-saucectl/#fastlane-plugin-saucectl)**.
24
23
 
25
- | Available Actions | Description |
26
- |-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
27
- | `install_saucectl` | Downloads the Sauce Labs saucectl cli binary for test execution. Optionally specify the [version](https://github.com/saucelabs/saucectl/releases/) you wish to install or automatically download the latest. |
28
- | `sauce_upload` | Upload test artifacts to sauce labs storage |
29
- | `sauce_config` | Create SauceLabs configuration file for test execution based on given parameters |
30
- | `sauce_runner` | Execute automated tests on sauce labs platform via saucectl binary for specified configuration |
31
- | `delete_from_storage` | Delete test artifacts from sauce labs storage by storage id or group id |
32
- | `sauce_apps` | Returns the set of files by specific app id that have been uploaded to Sauce Storage by the requester |
33
- | `sauce_devices` | Returns a list of Device IDs for all devices in the data center that are currently free for testing. |
34
- | `disabled_tests` | Fetches any disabled ui test cases (for android searches for @Ignore tests, and for ios skipped tests within an xcode test plan). Plan is to use this in the future for generating pretty HTML reports |
24
+ | Available Actions | Description |
25
+ |---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
26
+ | `install_saucectl` | Downloads the Sauce Labs saucectl cli binary for test execution |
27
+ | `sauce_upload` | Upload test artifacts to sauce labs storage |
28
+ | `sauce_config` | Create SauceLabs configuration file for test execution based on given parameters |
29
+ | `sauce_runner` | Execute automated tests on sauce labs platform via saucectl binary for specified configuration |
30
+ | `delete_from_storage` | Delete test artifacts from sauce labs storage by storage id or group id |
31
+ | `sauce_apps` | Returns the set of files by specific app id that have been uploaded to Sauce Storage by the requester |
32
+ | `sauce_devices` | Returns a list of Device IDs for all devices in the data center that are currently free for testing. |
33
+ | `disabled_tests` | Fetches any disabled ui test cases (for android searches for @Ignore tests, and for ios skipped tests within an xcode test plan). Plan is to use this in the future for generating pretty HTML reports |
34
+
35
+ An order of which you may utilize the above actions in your continuous integration platform could be:
36
+ 1. Install the saucectl binary via `install_saucectl`
37
+ 2. Upload your test artifacts to Sauce Labs storage (for example app apk, and test runner apk)
38
+ 3. Create config.yml for given parameters via `sauce_config`
39
+ 4. Execute test based on specified config via `sauce_runner`
40
+ 5. Delete test artifacts via `delete_from_storage` so that your storage does not fill up (if you're executing tests on every PR, for example)
35
41
 
36
42
  ## Example
37
43
 
@@ -4,11 +4,10 @@ require_relative '../helper/installer'
4
4
  module Fastlane
5
5
  module Actions
6
6
  class InstallSaucectlAction < Action
7
- def self.run(version = nil)
8
- version_message = version[:version].nil? ? 'Installing with latest version of saucectl' : "Installing saucectl with version #{version[:version]}"
9
- UI.message("#{version_message} 🤖 🚀")
7
+ def self.run(param = '')
8
+ UI.message("Installing saucectl 🤖 🚀")
10
9
  installer = Saucectl::Installer.new
11
- installer.install(version)
10
+ installer.install
12
11
  end
13
12
 
14
13
  def self.description
@@ -16,16 +15,7 @@ module Fastlane
16
15
  end
17
16
 
18
17
  def self.details
19
- "Optionally set the tag of the version you wish to install. If not tag is set, the latest tag will be downloaded. See: https://github.com/saucelabs/saucectl/tags "
20
- end
21
-
22
- def self.available_options
23
- [
24
- FastlaneCore::ConfigItem.new(key: :version,
25
- description: "Set the tag of saucectl you wish to install",
26
- optional: true,
27
- type: String)
28
- ]
18
+ "Installs the Sauce Labs saucectl cli binary"
29
19
  end
30
20
 
31
21
  def self.authors
@@ -75,19 +75,12 @@ module Fastlane
75
75
  FastlaneCore::ConfigItem.new(key: :test_distribution,
76
76
  description: "Test distribution method",
77
77
  optional: true,
78
- type: String),
78
+ type: String,
79
+ default_value: 'class'),
79
80
  FastlaneCore::ConfigItem.new(key: :test_class,
80
81
  description: "Array of tests to execute",
81
82
  optional: true,
82
83
  type: Array),
83
- FastlaneCore::ConfigItem.new(key: :size,
84
- description: "Instructs saucectl to run only tests that are annotated with the matching size value i.e @SmallTest, @MediumTest or @LargeTest. Valid values are small, medium, or large. You may only specify one value for this property",
85
- optional: true,
86
- type: String),
87
- FastlaneCore::ConfigItem.new(key: :annotation,
88
- description: "Instructs saucectl to run only tests that match a custom annotation that you have set",
89
- optional: true,
90
- type: String),
91
84
  FastlaneCore::ConfigItem.new(key: :emulators,
92
85
  description: "The parent property that defines details for running this suite on virtual devices using an emulator",
93
86
  optional: true,
@@ -163,7 +156,8 @@ module Fastlane
163
156
  FastlaneCore::ConfigItem.new(key: :path_to_tests,
164
157
  description: "Path to your espresso tests",
165
158
  optional: true,
166
- type: String),
159
+ type: String,
160
+ default_value: "#{Dir.pwd}/app/src/androidTest"),
167
161
  FastlaneCore::ConfigItem.new(key: :clear_data,
168
162
  description: "Clear package data from device (android only)",
169
163
  optional: true,
@@ -178,12 +172,7 @@ module Fastlane
178
172
  description: "Sets the maximum number of suites to execute at the same time. If the test defines more suites than the max, excess suites are queued and run in order as each suite completes",
179
173
  optional: true,
180
174
  type: Integer,
181
- default_value: 1),
182
- FastlaneCore::ConfigItem.new(key: :timeout,
183
- description: "Instructs how long (in ms, s, m, or h) saucectl should wait for each suite to complete. You can override this setting for individual suites using the timeout setting within the suites object. If not set, the default value is 0 (unlimited)",
184
- optional: true,
185
- type: String,
186
- default_value: '0')
175
+ default_value: 1)
187
176
  ]
188
177
  end
189
178
 
@@ -79,14 +79,6 @@ module Fastlane
79
79
  type: String,
80
80
  verify_block: proc do |value|
81
81
  UI.user_error!(@messages['sauce_api_key_error']) unless value && !value.empty?
82
- end),
83
- FastlaneCore::ConfigItem.new(key: :app_description,
84
- description: "A description of the artifact (optional, 1-255 chars)",
85
- optional: true,
86
- is_string: true,
87
- type: String,
88
- verify_block: proc do |value|
89
- UI.user_error!(@messages['description_malformed']) unless value && !value.empty? && value.to_s.length < 256
90
82
  end)
91
83
  ]
92
84
  end
@@ -67,15 +67,12 @@ module Fastlane
67
67
  end
68
68
 
69
69
  def upload
70
- UI.message("⏳ Uploading \"#{@config[:app]}\" to Sauce Labs.")
70
+ UI.message("⏳ Uploading \"#{@config[:app]}\" upload to Sauce Labs.")
71
71
  path = 'v1/storage/upload'
72
72
  https, url = build_http_request_for(path)
73
73
  request = Net::HTTP::Post.new(url)
74
74
  request['Authorization'] = "Basic #{@encoded_auth_string}"
75
75
  form_data = [['payload', File.open(@config[:file])], ['name', @config[:app]]]
76
- unless @config[:app_description].nil?
77
- form_data.append(['description', @config[:app_description]])
78
- end
79
76
  request.set_form(form_data, 'multipart/form-data')
80
77
  response = https.request(request)
81
78
  UI.success("✅ Successfully uploaded app to sauce labs: \n #{response.body}") if response.code.eql?('201')
@@ -5,8 +5,7 @@ require 'net/http'
5
5
  require 'json'
6
6
  require 'base64'
7
7
  require 'open3'
8
- require_relative 'ios_suites'
9
- require_relative 'android_suites'
8
+ require_relative 'suites'
10
9
 
11
10
  module Fastlane
12
11
  module Saucectl
@@ -24,13 +23,14 @@ module Fastlane
24
23
  {
25
24
  'apiVersion' => 'v1alpha',
26
25
  'kind' => @config[:kind],
27
- 'defaults' => {
28
- 'timeout' => @config[:timeout],
29
- },
26
+ 'retries' => @config[:retries],
30
27
  'sauce' => {
31
28
  'region' => set_region.to_s,
32
29
  'concurrency' => @config[:max_concurrency_size],
33
- 'retries' => @config[:retries]
30
+ 'metadata' => {
31
+ 'name' => "#{ENV['JOB_NAME']}-#{ENV['BUILD_NUMBER']}",
32
+ 'build' => "Release #{ENV['CI_COMMIT_SHORT_SHA']}"
33
+ }
34
34
  },
35
35
  (@config[:kind]).to_s => set_apps,
36
36
  'artifacts' => {
@@ -49,7 +49,12 @@ module Fastlane
49
49
  end
50
50
 
51
51
  def set_region
52
- @config[:region] == 'eu' ? 'eu-central-1' : 'us-west-1'
52
+ case @config[:region]
53
+ when 'eu'
54
+ 'eu-central-1'
55
+ else
56
+ 'us-west-1'
57
+ end
53
58
  end
54
59
 
55
60
  def set_apps
@@ -59,16 +64,12 @@ module Fastlane
59
64
  }
60
65
  end
61
66
 
62
- def suite
63
- @config[:platform].eql?('ios') ? Fastlane::Saucectl::IosSuites.new(@config) : Fastlane::Saucectl::AndroidSuites.new(@config)
64
- end
65
-
66
67
  def create
67
68
  UI.message("Creating saucectl config .....🚕💨")
68
69
  file_name = 'config.yml'
69
70
  UI.user_error!("❌ Sauce Labs platform does not support virtual device execution for ios apps") if @config[:platform].eql?('ios') && @config[:emulators]
70
71
 
71
- config = base_config.merge({ 'suites' => suite.generate })
72
+ config = base_config.merge(create_suite)
72
73
  out_file = File.new(file_name, 'w')
73
74
  out_file.puts(config.to_yaml)
74
75
  out_file.close
@@ -78,6 +79,15 @@ module Fastlane
78
79
  UI.user_error!("Failed to create saucectl config ❌") unless Dir.exist?('.sauce')
79
80
  end
80
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
+
81
91
  def creat_sauce_dir
82
92
  dirname = '.sauce'
83
93
  FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
@@ -5,7 +5,7 @@ require_relative "file_utils"
5
5
 
6
6
  module Fastlane
7
7
  module Saucectl
8
- # This class is responsible for creating test execution plans for android applications and will distribute tests
8
+ # This class is responsible for creating test execution plans for ios applications and will distribute tests
9
9
  # that will be be executed via the cloud provider.
10
10
  #
11
11
  class Espresso
@@ -12,11 +12,11 @@ module Fastlane
12
12
  include FileUtils
13
13
  UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
14
14
 
15
- def install(version)
16
- timeout_in_seconds = 90
15
+ def install
16
+ timeout_in_seconds = 30
17
17
  Timeout.timeout(timeout_in_seconds) do
18
18
  download_saucectl_installer
19
- execute_saucectl_installer(version)
19
+ execute_saucectl_installer
20
20
  UI.success("✅ Successfully installed saucectl runner binary 🚀")
21
21
  rescue OpenURI::HTTPError => e
22
22
  response = e.io
@@ -26,14 +26,13 @@ module Fastlane
26
26
 
27
27
  def download_saucectl_installer
28
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
29
+ file << URI.open('https://saucelabs.github.io/saucectl/install').read
30
30
  end
31
31
  end
32
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
33
+ def execute_saucectl_installer
34
+ status = system('sh sauce')
35
+ status == 1 ? UI.user_error!(" failed to install saucectl: #{stderr}") : status
37
36
  executable = 'saucectl'
38
37
  FileUtils.mv("bin/#{executable}", executable) unless File.exist?(executable)
39
38
  end
@@ -41,9 +40,7 @@ module Fastlane
41
40
  def system(*cmd)
42
41
  Open3.popen2e(*cmd) do |stdin, stdout_stderr, wait_thread|
43
42
  Thread.new do
44
- stdout_stderr.each do |out|
45
- UI.message(out)
46
- end
43
+ stdout_stderr.each { |out| UI.message(out) }
47
44
  end
48
45
  stdin.close
49
46
  wait_thread.value
@@ -25,10 +25,7 @@ module Fastlane
25
25
  def system(*cmd)
26
26
  Open3.popen2e(*cmd) do |stdin, stdout_stderr, wait_thread|
27
27
  Thread.new do
28
- stdout_stderr.each do |out|
29
- message = out.gsub(/(?:\[[^\]].*\])|(?:\(\d{4}\))/, '')
30
- puts(message)
31
- end
28
+ stdout_stderr.each { |out| UI.message(out) }
32
29
  end
33
30
  stdin.close
34
31
  wait_thread.value
@@ -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
@@ -64,11 +64,11 @@ module Fastlane
64
64
  case test_distribution
65
65
  when 'testCase', 'testPlan'
66
66
  test_data.each do |type|
67
- type[:tests].each { |test| tests_arr << "#{test_target.tr(" ", "_")}.#{type[:class]}/#{test}" }
67
+ type[:tests].each { |test| tests_arr << "#{test_target}.#{type[:class]}/#{test}" }
68
68
  end
69
69
  else
70
70
  test_data.each do |type|
71
- tests_arr << "#{test_target.tr(" ", "_")}.#{type[:class]}"
71
+ tests_arr << "#{test_target}.#{type[:class]}"
72
72
  end
73
73
  end
74
74
  tests_arr.uniq
@@ -10,4 +10,3 @@ sauce_api_key_error: "No sauce labs access key provided, set using: sauce_access
10
10
  supported_regions: ['us', 'eu']
11
11
  accepted_file_types: ['.apk', '.aab', '.ipa', '.zip']
12
12
  missing_file_name: "Please specify the name of the app that you wish to query on sauce storage"
13
- description_malformed: "Description was empty or > 255 characters. Do not set app_description or set using app_description: 'Foo'"
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Saucectl
3
- VERSION = '0.1.5'.freeze
3
+ VERSION = "0.1.6.pre"
4
4
  end
5
5
  end
@@ -1,9 +1,11 @@
1
- require 'fastlane/plugin/saucectl/version'
1
+ require 'fastlane'
2
2
 
3
3
  module Fastlane
4
4
  module Saucectl
5
+ UI = FastlaneCore::UI
6
+ # Return all .rb files inside the "actions" and "helper" directory
5
7
  def self.all_classes
6
- Dir[File.expand_path('*/{actions,helper}/*.rb', File.dirname(__FILE__))]
8
+ Dir[File.expand_path('**/actions/*_action.rb', File.dirname(__FILE__))]
7
9
  end
8
10
  end
9
11
  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.5
4
+ version: 0.1.6.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ian Hamilton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-26 00:00:00.000000000 Z
11
+ date: 2022-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -181,22 +181,22 @@ files:
181
181
  - lib/fastlane/plugin/saucectl/actions/sauce_devices_action.rb
182
182
  - lib/fastlane/plugin/saucectl/actions/sauce_runner_action.rb
183
183
  - lib/fastlane/plugin/saucectl/actions/sauce_upload_action.rb
184
- - lib/fastlane/plugin/saucectl/helper/android_suites.rb
185
184
  - lib/fastlane/plugin/saucectl/helper/api.rb
186
185
  - lib/fastlane/plugin/saucectl/helper/config.rb
187
186
  - lib/fastlane/plugin/saucectl/helper/espresso.rb
188
187
  - lib/fastlane/plugin/saucectl/helper/file_utils.rb
189
188
  - lib/fastlane/plugin/saucectl/helper/installer.rb
190
- - lib/fastlane/plugin/saucectl/helper/ios_suites.rb
191
189
  - lib/fastlane/plugin/saucectl/helper/runner.rb
192
190
  - lib/fastlane/plugin/saucectl/helper/storage.rb
191
+ - lib/fastlane/plugin/saucectl/helper/suites.rb
193
192
  - lib/fastlane/plugin/saucectl/helper/xctest.rb
194
193
  - lib/fastlane/plugin/saucectl/strings/messages.yml
195
194
  - lib/fastlane/plugin/saucectl/version.rb
196
195
  homepage: https://github.com/ianrhamilton/fastlane-plugin-saucectl
197
196
  licenses:
198
197
  - MIT
199
- metadata: {}
198
+ metadata:
199
+ rubygems_mfa_required: 'true'
200
200
  post_install_message:
201
201
  rdoc_options: []
202
202
  require_paths:
@@ -205,17 +205,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
205
205
  requirements:
206
206
  - - ">="
207
207
  - !ruby/object:Gem::Version
208
- version: '3.0'
208
+ version: '2.5'
209
209
  required_rubygems_version: !ruby/object:Gem::Requirement
210
210
  requirements:
211
- - - ">="
211
+ - - ">"
212
212
  - !ruby/object:Gem::Version
213
- version: '0'
213
+ version: 1.3.1
214
214
  requirements: []
215
- rubygems_version: 3.2.3
215
+ rubygems_version: 3.3.7
216
216
  signing_key:
217
217
  specification_version: 4
218
218
  summary: Simplify the set up, configuration, upload, and execution of espresso and
219
219
  XCUITest on the Sauce Labs platform by utilizing fastlane which will enable you
220
- to test your iOS and Android apps at scale.
220
+ to test your iOS and Android apps at scale
221
221
  test_files: []
@@ -1,180 +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 AndroidSuites
13
- include FileUtils
14
-
15
- UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
16
-
17
- def initialize(config)
18
- UI.user_error!("❌ #{config[:kind]} is not a supported test framework for android. Use espresso") unless config[:kind].eql?('espresso')
19
- @devices = config[:devices].nil? ? config[:emulators] : config[:devices]
20
- @is_real_device = config[:emulators].nil?
21
- @config = config
22
- end
23
-
24
- def generate
25
- if @config[:test_distribution] || @config[:size] || @config[:annotation]
26
- create_test_distribution_suite
27
- elsif @config[:test_class].kind_of?(Array)
28
- custom_test_class_suite
29
- else
30
- test_runner_suite
31
- end
32
- end
33
-
34
- def test_distribution_array
35
- @config[:test_class] || Fastlane::Saucectl::Espresso.new(@config).test_distribution
36
- end
37
-
38
- def suite_name(test_type)
39
- if ENV['JOB_NAME'].nil? && ENV['BUILD_NUMBER'].nil?
40
- "#{@config[:kind]}-#{test_type}"
41
- else
42
- "#{ENV['JOB_NAME']}-#{ENV['BUILD_NUMBER']}-#{test_type}"
43
- end
44
- end
45
-
46
- def shard_suites
47
- UI.user_error!("❌ Cannot split #{@config[:test_distribution]}'s across devices with a single device/emulator. \nPlease specify a minimum of two devices/emulators!") if @devices.size.eql?(1)
48
-
49
- test_suites = []
50
- arr = test_distribution_array
51
- shards = arr.each_slice((arr.size / @devices.size.to_f).round).to_a
52
- shards.each_with_index do |suite, i|
53
- device_options = @is_real_device ? real_device_options(@devices[i]) : virtual_device_options(@devices[i])
54
- test_suites << {
55
- 'name' => suite_name("shard #{i + 1}").downcase,
56
- 'testOptions' => test_option_type(suite)
57
- }.merge(device_options)
58
- end
59
- test_suites
60
- end
61
-
62
- def test_runner_suite
63
- test_suites = []
64
- @devices.each do |device|
65
- device_options = @is_real_device ? real_device_options(device) : virtual_device_options(device)
66
- device_name = device.key?(:id) ? device[:id] : device[:name]
67
- test_suites << {
68
- 'name' => suite_name(device_name),
69
- 'testOptions' => test_option_type
70
- }.merge(device_options)
71
- end
72
- test_suites
73
- end
74
-
75
- def custom_test_class_suite
76
- test_suites = []
77
- @devices.each do |device|
78
- device_options = @is_real_device ? real_device_options(device) : virtual_device_options(device)
79
- test_classes = @config[:test_class].reject(&:empty?).join(',')
80
- test_suites << {
81
- 'name' => suite_name(device[:name]).downcase,
82
- 'testOptions' => test_option_type(test_classes.split(','))
83
- }.merge(device_options)
84
- end
85
- test_suites
86
- end
87
-
88
- def create_test_distribution_suite
89
- if @config[:test_distribution].eql?('shard')
90
- shard_suites
91
- else
92
- test_distribution_suite
93
- end
94
- end
95
-
96
- def test_distribution_suite
97
- UI.user_error!("❌ to distribute tests you must specify the path to your espresso tests. For example `path_to_tests:someModule/src/androidTest`") if @config[:path_to_tests].nil?
98
-
99
- test_suites = []
100
- if @config[:size] || @config[:annotation]
101
- @devices.each do |device|
102
- device_options = @is_real_device ? real_device_options(device) : virtual_device_options(device)
103
- test_suites << {
104
- 'name' => suite_name(device[:name]).downcase,
105
- 'testOptions' => test_option_type
106
- }.merge(device_options)
107
- end
108
- else
109
- @devices.each do |device|
110
- device_options = @is_real_device ? real_device_options(device) : virtual_device_options(device)
111
- test_distribution_array.each do |test_type|
112
- test_suites << {
113
- 'name' => suite_name(test_type).downcase,
114
- 'testOptions' => test_option_type(test_type)
115
- }.merge(device_options)
116
- end
117
- end
118
- end
119
- test_suites
120
- end
121
-
122
- def virtual_device_options(device)
123
- platform_versions = device[:platform_versions].reject(&:empty?).join(',')
124
- { 'emulators' => [{ 'name' => device[:name],
125
- 'orientation' => device[:orientation],
126
- 'platformVersions' => platform_versions.split(',') }] }
127
- end
128
-
129
- def real_device_options(device)
130
- { 'devices' => [rdc_options(device)] }
131
- end
132
-
133
- def rdc_options(device)
134
- device_type_key = device.key?(:id) ? 'id' : 'name'
135
- name = device.key?(:id) ? device[:id] : device[:name]
136
-
137
- base_device_hash = {
138
- device_type_key => name,
139
- 'orientation' => device[:orientation]
140
- }.merge('options' => device_options(device))
141
-
142
- unless device[:platform_version].nil?
143
- base_device_hash = base_device_hash.merge({ 'platformVersion' => device[:platform_version] })
144
- end
145
-
146
- base_device_hash
147
- end
148
-
149
- def device_options(device)
150
- {
151
- 'carrierConnectivity' => device[:carrier_connectivity],
152
- 'deviceType' => device[:device_type].upcase!,
153
- 'private' => device[:private]
154
- }
155
- end
156
-
157
- def test_option_type(test_type = nil)
158
- if @config[:size] || @config[:annotation]
159
- key = @config[:size] ? 'size' : 'annotation'
160
- value = @config[:size] || @config[:annotation]
161
- { key => value }.merge(android_test_options)
162
- else
163
- if test_type.nil?
164
- android_test_options
165
- else
166
- test_option_type = @config[:test_distribution].eql?('package') ? 'package' : 'class'
167
- { test_option_type => test_type }.merge(android_test_options)
168
- end
169
- end
170
- end
171
-
172
- def android_test_options
173
- {
174
- 'clearPackageData' => @config[:clear_data],
175
- 'useTestOrchestrator' => @config[:use_test_orchestrator]
176
- }
177
- end
178
- end
179
- end
180
- end
@@ -1,166 +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 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