fastlane-plugin-saucectl 0.1.2.pre → 0.1.3

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: 2bb0bc0c918e497c8c4ec0d560635d0a059bed8d5456c6df34f33a5aa626d9ba
4
- data.tar.gz: 9f4626de2b041b65e8fd0882726f29ef1053ed8054499a704b13ca96773300fc
3
+ metadata.gz: 6ab968db699553cb2d744fe26a66dcd3e02b56e796935092b408d04ec2b5566b
4
+ data.tar.gz: c5c3cbf2f72e159be64c0a47f025e11136c24285260488a12699322f88892caf
5
5
  SHA512:
6
- metadata.gz: 14a4fdcd1d5ce125a1d08af53d51f5c82fcc44cdd4dbab14e315cd6b417daedd41d7c7be6c3a8d420211b6697bf340e40fb1749331f6aff34db9c930f4365967
7
- data.tar.gz: c07918fee9b5067146ea9b840d11c495218bb21229cf22221ed217a25574beacfba97902fffb4fc24484d13d1a89a267dcc4d1524d2946e1db7a026166e96380
6
+ metadata.gz: c26a558fa620c0c8f6cc20f9739b6ad7e8dc5382b1a82b88a2439b10f9621fae7d45a92b54c085c2d19a2b249f2d75155ef96d67d0051ef223b4d36cdffa3497
7
+ data.tar.gz: cc8dfb29b8d3576dffba855245a140e43a3809ea337c0ddf5c8a1d14844b5f31f9a89d254c159127e70647c5f6593a1659f4d9a5d2724c6bde2f8cb2562de559
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Ian Hamilton <ian.ross.hamilton@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # fastlane-plugin-saucectl
2
+
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)
6
+
7
+ ## Getting Started
8
+
9
+ This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To get started with `fastlane-plugin-saucectl`, add it to your project by running:
10
+
11
+ ```bash
12
+ fastlane add_plugin saucectl
13
+ ```
14
+
15
+ ## About fastlane-plugin-saucectl
16
+
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.
18
+
19
+ **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
+
21
+ Failure to do this will result in missing test classes and test cases from your test run.
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)**.
24
+
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 |
35
+
36
+ An order of which you may utilize the above actions in your continuous integration platform could be:
37
+ 1. Install the saucectl binary via `install_saucectl`
38
+ 2. Upload your test artifacts to Sauce Labs storage (for example app apk, and test runner apk)
39
+ 3. Create config.yml for given parameters via `sauce_config`
40
+ 4. Execute test based on specified config via `sauce_runner`
41
+ 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)
42
+
43
+ ## Example
44
+
45
+ Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use this plugin. Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`.
46
+
47
+ **Note to author:** Please set up a sample project to make it easy for users to explore what your plugin does. Provide everything that is necessary to try out the plugin in this project (including a sample Xcode/Android project if necessary)
48
+
49
+ ## Run tests for this plugin
50
+
51
+ To run both the tests, and code style validation, run
52
+
53
+ ```
54
+ rake
55
+ ```
56
+
57
+ To automatically fix many of the styling issues, use
58
+ ```
59
+ rubocop -a
60
+ ```
61
+
62
+ ## Issues and Feedback
63
+
64
+ For any other issues and feedback about this plugin, please submit it to this repository.
65
+
66
+ ## Troubleshooting
67
+
68
+ If you have trouble using plugins, check out the [Plugins Troubleshooting](https://docs.fastlane.tools/plugins/plugins-troubleshooting/) guide.
69
+
70
+ ## Using _fastlane_ Plugins
71
+
72
+ For more information about how the `fastlane` plugin system works, check out the [Plugins documentation](https://docs.fastlane.tools/plugins/create-plugin/).
73
+
74
+ ## About _fastlane_
75
+
76
+ _fastlane_ is the easiest way to automate beta deployments and releases for your iOS and Android apps. To learn more, check out [fastlane.tools](https://fastlane.tools).
77
+
78
+ ## Buy me a coffee
79
+ If you're enjoying this plugin, feel free to **optionally** buy me a coffee :)
80
+
81
+ [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/ianrhamilton)
@@ -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,
@@ -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
@@ -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
@@ -26,11 +27,7 @@ module Fastlane
26
27
  'retries' => @config[:retries],
27
28
  'sauce' => {
28
29
  'region' => set_region.to_s,
29
- 'concurrency' => @config[:max_concurrency_size],
30
- 'metadata' => {
31
- 'name' => "#{ENV['JOB_NAME']}-#{ENV['BUILD_NUMBER']}",
32
- 'build' => "Release #{ENV['CI_COMMIT_SHORT_SHA']}"
33
- }
30
+ 'concurrency' => @config[:max_concurrency_size]
34
31
  },
35
32
  (@config[:kind]).to_s => set_apps,
36
33
  'artifacts' => {
@@ -49,12 +46,7 @@ module Fastlane
49
46
  end
50
47
 
51
48
  def set_region
52
- case @config[:region]
53
- when 'eu'
54
- 'eu-central-1'
55
- else
56
- 'us-west-1'
57
- end
49
+ @config[:region] == 'eu' ? 'eu-central-1' : 'us-west-1'
58
50
  end
59
51
 
60
52
  def set_apps
@@ -64,12 +56,16 @@ module Fastlane
64
56
  }
65
57
  end
66
58
 
59
+ def suite
60
+ @config[:platform].eql?('ios') ? Fastlane::Saucectl::IosSuites.new(@config) : Fastlane::Saucectl::AndroidSuites.new(@config)
61
+ end
62
+
67
63
  def create
68
64
  UI.message("Creating saucectl config .....🚕💨")
69
65
  file_name = 'config.yml'
70
66
  UI.user_error!("❌ Sauce Labs platform does not support virtual device execution for ios apps") if @config[:platform].eql?('ios') && @config[:emulators]
71
67
 
72
- config = base_config.merge(create_suite)
68
+ config = base_config.merge({ 'suites' => suite.generate })
73
69
  out_file = File.new(file_name, 'w')
74
70
  out_file.puts(config.to_yaml)
75
71
  out_file.close
@@ -79,15 +75,6 @@ module Fastlane
79
75
  UI.user_error!("Failed to create saucectl config ❌") unless Dir.exist?('.sauce')
80
76
  end
81
77
 
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
78
  def creat_sauce_dir
92
79
  dirname = '.sauce'
93
80
  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
@@ -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"
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Saucectl
3
- VERSION = "0.1.2.pre"
3
+ VERSION = '0.1.3'.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.2.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
@@ -170,6 +170,8 @@ executables: []
170
170
  extensions: []
171
171
  extra_rdoc_files: []
172
172
  files:
173
+ - LICENSE
174
+ - README.md
173
175
  - lib/fastlane/plugin/saucectl.rb
174
176
  - lib/fastlane/plugin/saucectl/actions/delete_from_storage_action.rb
175
177
  - lib/fastlane/plugin/saucectl/actions/disabled_tests_action.rb
@@ -179,15 +181,17 @@ files:
179
181
  - lib/fastlane/plugin/saucectl/actions/sauce_devices_action.rb
180
182
  - lib/fastlane/plugin/saucectl/actions/sauce_runner_action.rb
181
183
  - lib/fastlane/plugin/saucectl/actions/sauce_upload_action.rb
184
+ - lib/fastlane/plugin/saucectl/helper/android_suites.rb
182
185
  - lib/fastlane/plugin/saucectl/helper/api.rb
183
186
  - lib/fastlane/plugin/saucectl/helper/config.rb
184
187
  - lib/fastlane/plugin/saucectl/helper/espresso.rb
185
188
  - lib/fastlane/plugin/saucectl/helper/file_utils.rb
186
189
  - lib/fastlane/plugin/saucectl/helper/installer.rb
190
+ - lib/fastlane/plugin/saucectl/helper/ios_suites.rb
187
191
  - lib/fastlane/plugin/saucectl/helper/runner.rb
188
192
  - lib/fastlane/plugin/saucectl/helper/storage.rb
189
- - lib/fastlane/plugin/saucectl/helper/suites.rb
190
193
  - lib/fastlane/plugin/saucectl/helper/xctest.rb
194
+ - lib/fastlane/plugin/saucectl/strings/messages.yml
191
195
  - lib/fastlane/plugin/saucectl/version.rb
192
196
  homepage: https://github.com/ianrhamilton/fastlane-plugin-saucectl
193
197
  licenses:
@@ -202,15 +206,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
202
206
  requirements:
203
207
  - - ">="
204
208
  - !ruby/object:Gem::Version
205
- version: '2.6'
209
+ version: '2.5'
206
210
  required_rubygems_version: !ruby/object:Gem::Requirement
207
211
  requirements:
208
- - - ">"
212
+ - - ">="
209
213
  - !ruby/object:Gem::Version
210
- version: 1.3.1
214
+ version: '0'
211
215
  requirements: []
212
- rubygems_version: 3.3.7
216
+ rubygems_version: 3.3.10
213
217
  signing_key:
214
218
  specification_version: 4
215
- 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.
216
222
  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