fastlane-plugin-saucectl 0.1.5.pre → 0.1.5

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: 0e8ebb5a262c386ebc64f2964179acfd65354df626d62b418a29a8ab3d41a0f5
4
- data.tar.gz: 1682d0941237bad34d29b012fcddccbb4cfa2d7da16d5412dcb6d330202bb67a
3
+ metadata.gz: 637b1e3c3ccf3d188af05d598d348dff048e3877d5ac2ae1a16cf1829c299a3b
4
+ data.tar.gz: 4db08970b9bd2638bae2aabfda7eddbc753e0820fcbebdf4357676affb172f1b
5
5
  SHA512:
6
- metadata.gz: 26c29ac5d4aad6bb912533b43c382cd63418d890a5d467122679d043bf3a3c1b074895761fba59e21b723dc3f20f1add2139ce4ad64ed44884d9839c03458630
7
- data.tar.gz: 77e8448cb56e675c049a6be97bd04b6b3d3499dddd3d930bf21193d962b9eda2e50165fe218ef9eed9f345300bf0b9e04b2decd136f98e4564104c99fef3dfe5
6
+ metadata.gz: da201db10c50520c11c47eaf262c6e31cc7d62deb51d3e66e0abd740fb691f46b9ab28d8cc2afc2e480621d537403fef298f2f1080398054a7bbabc8bb070c1b
7
+ data.tar.gz: 556f168359742f5243d45a98af0ee7c4770172f7df174a17e43ba7824a3b8b0978ce26978f47c558e9f4afd65aa2cb5f749e40b1224a83907aa5deb912689fb7
data/README.md CHANGED
@@ -1,7 +1,8 @@
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
- [![codecov](https://codecov.io/gh/ianrhamilton/fastlane-plugin-rsaucectl/branch/main/graph/badge.svg?token=NSVhqgYFYv)](https://codecov.io/gh/ianrhamilton/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)
5
6
 
6
7
  ## Getting Started
7
8
 
@@ -13,31 +14,24 @@ fastlane add_plugin saucectl
13
14
 
14
15
  ## About fastlane-plugin-saucectl
15
16
 
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.
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.
17
18
 
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.
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.
19
20
 
20
21
  Failure to do this will result in missing test classes and test cases from your test run.
21
22
 
22
23
  **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)**.
23
24
 
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)
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 |
41
35
 
42
36
  ## Example
43
37
 
@@ -4,10 +4,11 @@ require_relative '../helper/installer'
4
4
  module Fastlane
5
5
  module Actions
6
6
  class InstallSaucectlAction < Action
7
- def self.run(param = '')
8
- UI.message("Installing saucectl 🤖 🚀")
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} 🤖 🚀")
9
10
  installer = Saucectl::Installer.new
10
- installer.install
11
+ installer.install(version)
11
12
  end
12
13
 
13
14
  def self.description
@@ -15,7 +16,16 @@ module Fastlane
15
16
  end
16
17
 
17
18
  def self.details
18
- "Installs the Sauce Labs saucectl cli binary"
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
+ ]
19
29
  end
20
30
 
21
31
  def self.authors
@@ -75,12 +75,19 @@ module Fastlane
75
75
  FastlaneCore::ConfigItem.new(key: :test_distribution,
76
76
  description: "Test distribution method",
77
77
  optional: true,
78
- type: String,
79
- default_value: 'class'),
78
+ type: String),
80
79
  FastlaneCore::ConfigItem.new(key: :test_class,
81
80
  description: "Array of tests to execute",
82
81
  optional: true,
83
82
  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),
84
91
  FastlaneCore::ConfigItem.new(key: :emulators,
85
92
  description: "The parent property that defines details for running this suite on virtual devices using an emulator",
86
93
  optional: true,
@@ -156,8 +163,7 @@ module Fastlane
156
163
  FastlaneCore::ConfigItem.new(key: :path_to_tests,
157
164
  description: "Path to your espresso tests",
158
165
  optional: true,
159
- type: String,
160
- default_value: "#{Dir.pwd}/app/src/androidTest"),
166
+ type: String),
161
167
  FastlaneCore::ConfigItem.new(key: :clear_data,
162
168
  description: "Clear package data from device (android only)",
163
169
  optional: true,
@@ -172,7 +178,12 @@ module Fastlane
172
178
  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",
173
179
  optional: true,
174
180
  type: Integer,
175
- default_value: 1)
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')
176
187
  ]
177
188
  end
178
189
 
@@ -79,6 +79,14 @@ 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
82
90
  end)
83
91
  ]
84
92
  end
@@ -0,0 +1,180 @@
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
@@ -67,12 +67,15 @@ module Fastlane
67
67
  end
68
68
 
69
69
  def upload
70
- UI.message("⏳ Uploading \"#{@config[:app]}\" upload to Sauce Labs.")
70
+ UI.message("⏳ Uploading \"#{@config[:app]}\" 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
76
79
  request.set_form(form_data, 'multipart/form-data')
77
80
  response = https.request(request)
78
81
  UI.success("✅ Successfully uploaded app to sauce labs: \n #{response.body}") if response.code.eql?('201')
@@ -5,7 +5,8 @@ require 'net/http'
5
5
  require 'json'
6
6
  require 'base64'
7
7
  require 'open3'
8
- require_relative 'suites'
8
+ require_relative 'ios_suites'
9
+ require_relative 'android_suites'
9
10
 
10
11
  module Fastlane
11
12
  module Saucectl
@@ -23,14 +24,13 @@ module Fastlane
23
24
  {
24
25
  'apiVersion' => 'v1alpha',
25
26
  'kind' => @config[:kind],
26
- 'retries' => @config[:retries],
27
+ 'defaults' => {
28
+ 'timeout' => @config[:timeout],
29
+ },
27
30
  'sauce' => {
28
31
  'region' => set_region.to_s,
29
32
  'concurrency' => @config[:max_concurrency_size],
30
- 'metadata' => {
31
- 'name' => "#{ENV['JOB_NAME']}-#{ENV['BUILD_NUMBER']}",
32
- 'build' => "Release #{ENV['CI_COMMIT_SHORT_SHA']}"
33
- }
33
+ 'retries' => @config[:retries]
34
34
  },
35
35
  (@config[:kind]).to_s => set_apps,
36
36
  'artifacts' => {
@@ -49,12 +49,7 @@ module Fastlane
49
49
  end
50
50
 
51
51
  def set_region
52
- case @config[:region]
53
- when 'eu'
54
- 'eu-central-1'
55
- else
56
- 'us-west-1'
57
- end
52
+ @config[:region] == 'eu' ? 'eu-central-1' : 'us-west-1'
58
53
  end
59
54
 
60
55
  def set_apps
@@ -64,12 +59,16 @@ module Fastlane
64
59
  }
65
60
  end
66
61
 
62
+ def suite
63
+ @config[:platform].eql?('ios') ? Fastlane::Saucectl::IosSuites.new(@config) : Fastlane::Saucectl::AndroidSuites.new(@config)
64
+ end
65
+
67
66
  def create
68
67
  UI.message("Creating saucectl config .....🚕💨")
69
68
  file_name = 'config.yml'
70
69
  UI.user_error!("❌ Sauce Labs platform does not support virtual device execution for ios apps") if @config[:platform].eql?('ios') && @config[:emulators]
71
70
 
72
- config = base_config.merge(create_suite)
71
+ config = base_config.merge({ 'suites' => suite.generate })
73
72
  out_file = File.new(file_name, 'w')
74
73
  out_file.puts(config.to_yaml)
75
74
  out_file.close
@@ -79,15 +78,6 @@ module Fastlane
79
78
  UI.user_error!("Failed to create saucectl config ❌") unless Dir.exist?('.sauce')
80
79
  end
81
80
 
82
- def create_suite
83
- suite = Fastlane::Saucectl::Suites.new(@config)
84
- { 'suites' => if @config[:emulators]
85
- suite.create_virtual_device_suites
86
- else
87
- suite.create_real_device_suites
88
- end }
89
- end
90
-
91
81
  def creat_sauce_dir
92
82
  dirname = '.sauce'
93
83
  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 ios applications and will distribute tests
8
+ # This class is responsible for creating test execution plans for android 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
16
- timeout_in_seconds = 30
15
+ def install(version)
16
+ timeout_in_seconds = 90
17
17
  Timeout.timeout(timeout_in_seconds) do
18
18
  download_saucectl_installer
19
- execute_saucectl_installer
19
+ execute_saucectl_installer(version)
20
20
  UI.success("✅ Successfully installed saucectl runner binary 🚀")
21
21
  rescue OpenURI::HTTPError => e
22
22
  response = e.io
@@ -26,13 +26,14 @@ 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').read
29
+ file << URI.open('https://saucelabs.github.io/saucectl/install', ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE).read
30
30
  end
31
31
  end
32
32
 
33
- def execute_saucectl_installer
34
- status = system('sh sauce')
35
- status == 1 ? UI.user_error!(" failed to install saucectl: #{stderr}") : status
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
36
37
  executable = 'saucectl'
37
38
  FileUtils.mv("bin/#{executable}", executable) unless File.exist?(executable)
38
39
  end
@@ -40,7 +41,9 @@ module Fastlane
40
41
  def system(*cmd)
41
42
  Open3.popen2e(*cmd) do |stdin, stdout_stderr, wait_thread|
42
43
  Thread.new do
43
- stdout_stderr.each { |out| UI.message(out) }
44
+ stdout_stderr.each do |out|
45
+ UI.message(out)
46
+ end
44
47
  end
45
48
  stdin.close
46
49
  wait_thread.value
@@ -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
@@ -25,7 +25,10 @@ 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 { |out| UI.message(out) }
28
+ stdout_stderr.each do |out|
29
+ message = out.gsub(/(?:\[[^\]].*\])|(?:\(\d{4}\))/, '')
30
+ puts(message)
31
+ end
29
32
  end
30
33
  stdin.close
31
34
  wait_thread.value
@@ -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}.#{type[:class]}/#{test}" }
67
+ type[:tests].each { |test| tests_arr << "#{test_target.tr(" ", "_")}.#{type[:class]}/#{test}" }
68
68
  end
69
69
  else
70
70
  test_data.each do |type|
71
- tests_arr << "#{test_target}.#{type[:class]}"
71
+ tests_arr << "#{test_target.tr(" ", "_")}.#{type[:class]}"
72
72
  end
73
73
  end
74
74
  tests_arr.uniq
@@ -10,3 +10,4 @@ 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.pre"
3
+ VERSION = '0.1.5'.freeze
4
4
  end
5
5
  end
@@ -1,11 +1,9 @@
1
- require 'fastlane'
1
+ require 'fastlane/plugin/saucectl/version'
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
7
5
  def self.all_classes
8
- Dir[File.expand_path('**/actions/*_action.rb', File.dirname(__FILE__))]
6
+ Dir[File.expand_path('*/{actions,helper}/*.rb', File.dirname(__FILE__))]
9
7
  end
10
8
  end
11
9
  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.pre
4
+ version: 0.1.5
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: 2024-01-26 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
184
185
  - lib/fastlane/plugin/saucectl/helper/api.rb
185
186
  - lib/fastlane/plugin/saucectl/helper/config.rb
186
187
  - lib/fastlane/plugin/saucectl/helper/espresso.rb
187
188
  - lib/fastlane/plugin/saucectl/helper/file_utils.rb
188
189
  - lib/fastlane/plugin/saucectl/helper/installer.rb
190
+ - lib/fastlane/plugin/saucectl/helper/ios_suites.rb
189
191
  - lib/fastlane/plugin/saucectl/helper/runner.rb
190
192
  - lib/fastlane/plugin/saucectl/helper/storage.rb
191
- - lib/fastlane/plugin/saucectl/helper/suites.rb
192
193
  - lib/fastlane/plugin/saucectl/helper/xctest.rb
193
194
  - lib/fastlane/plugin/saucectl/strings/messages.yml
194
195
  - lib/fastlane/plugin/saucectl/version.rb
195
196
  homepage: https://github.com/ianrhamilton/fastlane-plugin-saucectl
196
197
  licenses:
197
198
  - MIT
198
- metadata:
199
- rubygems_mfa_required: 'true'
199
+ metadata: {}
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: '2.6'
208
+ version: '3.0'
209
209
  required_rubygems_version: !ruby/object:Gem::Requirement
210
210
  requirements:
211
- - - ">"
211
+ - - ">="
212
212
  - !ruby/object:Gem::Version
213
- version: 1.3.1
213
+ version: '0'
214
214
  requirements: []
215
- rubygems_version: 3.3.7
215
+ rubygems_version: 3.2.3
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,201 +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 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