fastlane-plugin-saucectl 0.1.3.pre → 0.1.6.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'open-uri'
6
+ require 'json'
7
+ require 'base64'
8
+ require 'timeout'
9
+ require 'fastlane_core/ui/ui'
10
+ require 'fileutils'
11
+
12
+ module Fastlane
13
+ module Saucectl
14
+ #
15
+ # This class provides the functions required to interact with the saucectl api
16
+ # for more information see: https://docs.saucelabs.com/dev/api/storage/
17
+ #
18
+ class Api
19
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
20
+
21
+ def initialize(config)
22
+ @config = config
23
+ @encoded_auth_string = Base64.strict_encode64("#{@config[:sauce_username]}:#{@config[:sauce_access_key]}")
24
+ @messages = YAML.load_file("#{__dir__}/../strings/messages.yml")
25
+ end
26
+
27
+ def available_devices
28
+ path = 'v1/rdc/devices/available'
29
+ https, url = build_http_request_for(path)
30
+ request = Net::HTTP::Get.new(url)
31
+ request['Authorization'] = "Basic #{@encoded_auth_string}"
32
+ response = https.request(request)
33
+ UI.user_error!("❌ Request failed: #{response.code} #{response.message}") unless response.kind_of?(Net::HTTPOK)
34
+
35
+ JSON.parse(response.body)
36
+ end
37
+
38
+ def fetch_ios_devices
39
+ devices = []
40
+ get_devices = available_devices
41
+ get_devices.each do |device|
42
+ devices << device if device =~ /iPhone_.*/ || device =~ /iPad_.*/
43
+ end
44
+ devices
45
+ end
46
+
47
+ def fetch_android_devices
48
+ devices = []
49
+ get_devices = available_devices
50
+ get_devices.each do |device|
51
+ devices << device unless device =~ /iPhone_.*/ || device =~ /iPad_.*/
52
+ end
53
+ devices
54
+ end
55
+
56
+ def retrieve_all_apps
57
+ UI.message("retrieving all apps for \"#{@config[:query]}\".")
58
+ path = "v1/storage/files?q=#{@config[:query]}&kind=#{@config[:platform]}"
59
+ https, url = build_http_request_for(path)
60
+ request = Net::HTTP::Get.new(url)
61
+ request['Authorization'] = "Basic #{@encoded_auth_string}"
62
+ response = https.request(request)
63
+
64
+ UI.user_error!("❌ Request failed: #{response.code} #{response.message}") unless response.kind_of?(Net::HTTPOK)
65
+
66
+ response
67
+ end
68
+
69
+ def upload
70
+ UI.message("⏳ Uploading \"#{@config[:app]}\" upload to Sauce Labs.")
71
+ path = 'v1/storage/upload'
72
+ https, url = build_http_request_for(path)
73
+ request = Net::HTTP::Post.new(url)
74
+ request['Authorization'] = "Basic #{@encoded_auth_string}"
75
+ form_data = [['payload', File.open(@config[:file])], ['name', @config[:app]]]
76
+ request.set_form(form_data, 'multipart/form-data')
77
+ response = https.request(request)
78
+ UI.success("✅ Successfully uploaded app to sauce labs: \n #{response.body}") if response.code.eql?('201')
79
+ UI.user_error!("❌ Request failed: #{response.code} #{response.message}") unless response.code.eql?('201')
80
+
81
+ response
82
+ end
83
+
84
+ def delete_app(path)
85
+ https, url = build_http_request_for(path)
86
+ request = Net::HTTP::Delete.new(url.path)
87
+ request['Authorization'] = "Basic #{@encoded_auth_string}"
88
+ response = https.request(request)
89
+ UI.success("✅ Successfully deleted app from sauce labs storage: \n #{response.body}") if response.kind_of?(Net::HTTPOK)
90
+ UI.user_error!("❌ Request failed: #{response.code} #{response.message}") unless response.kind_of?(Net::HTTPOK)
91
+
92
+ response
93
+ end
94
+
95
+ def base_url_for_region
96
+ case @config[:region]
97
+ when 'eu' then base_url('eu-central-1')
98
+ when 'us' then base_url('us-west-1')
99
+ else UI.user_error!("#{@config[:region]} is an invalid region ❌. Available: #{@messages['supported_regions']}")
100
+ end
101
+ end
102
+
103
+ def build_http_request_for(path)
104
+ url = URI("#{base_url_for_region}/#{path}")
105
+ https = Net::HTTP.new(url.host, url.port)
106
+ https.use_ssl = true
107
+ [https, url]
108
+ end
109
+
110
+ def base_url(region)
111
+ "https://api.#{region}.saucelabs.com"
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,97 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'json'
6
+ require 'base64'
7
+ require 'open3'
8
+ require_relative 'suites'
9
+
10
+ module Fastlane
11
+ module Saucectl
12
+ #
13
+ # This class creates saucectl config.yml file based on given specifications
14
+ #
15
+ class ConfigGenerator
16
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
17
+
18
+ def initialize(config)
19
+ @config = config
20
+ end
21
+
22
+ def base_config
23
+ {
24
+ 'apiVersion' => 'v1alpha',
25
+ 'kind' => @config[:kind],
26
+ 'retries' => @config[:retries],
27
+ 'sauce' => {
28
+ '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
+ }
34
+ },
35
+ (@config[:kind]).to_s => set_apps,
36
+ 'artifacts' => {
37
+ 'download' => {
38
+ 'when' => 'always',
39
+ 'match' => ['junit.xml'],
40
+ 'directory' => './artifacts/'
41
+ }
42
+ },
43
+ 'reporters' => {
44
+ 'junit' => {
45
+ 'enabled' => true
46
+ }
47
+ }
48
+ }
49
+ end
50
+
51
+ def set_region
52
+ case @config[:region]
53
+ when 'eu'
54
+ 'eu-central-1'
55
+ else
56
+ 'us-west-1'
57
+ end
58
+ end
59
+
60
+ def set_apps
61
+ {
62
+ 'app' => @config[:app],
63
+ 'testApp' => @config[:test_app]
64
+ }
65
+ end
66
+
67
+ def create
68
+ UI.message("Creating saucectl config .....🚕💨")
69
+ file_name = 'config.yml'
70
+ UI.user_error!("❌ Sauce Labs platform does not support virtual device execution for ios apps") if @config[:platform].eql?('ios') && @config[:emulators]
71
+
72
+ config = base_config.merge(create_suite)
73
+ out_file = File.new(file_name, 'w')
74
+ out_file.puts(config.to_yaml)
75
+ out_file.close
76
+ creat_sauce_dir
77
+ FileUtils.move(file_name, './.sauce')
78
+ UI.message("Successfully created saucectl config ✅") if Dir.exist?('.sauce')
79
+ UI.user_error!("Failed to create saucectl config ❌") unless Dir.exist?('.sauce')
80
+ end
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
+
91
+ def creat_sauce_dir
92
+ dirname = '.sauce'
93
+ FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,93 @@
1
+ require "find"
2
+ require "open3"
3
+ require "json"
4
+ require_relative "file_utils"
5
+
6
+ module Fastlane
7
+ module Saucectl
8
+ # This class is responsible for creating test execution plans for ios applications and will distribute tests
9
+ # that will be be executed via the cloud provider.
10
+ #
11
+ class Espresso
12
+ include FileUtils
13
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
14
+
15
+ TEST_FUNCTION_REGEX = /([a-z]+[A-Z][a-zA-Z]+)[(][)]/.freeze
16
+
17
+ def initialize(config)
18
+ @config = config
19
+ end
20
+
21
+ def test_data
22
+ test_details = []
23
+ search_retrieve_test_classes(@config[:path_to_tests]).each do |f|
24
+ next unless File.basename(f) =~ CLASS_NAME_REGEX
25
+
26
+ test_details << { package: File.readlines(f).first.chomp.gsub("package ", "").gsub(";", ""),
27
+ class: File.basename(f).gsub(FILE_TYPE_REGEX, ""),
28
+ tests: tests_from(f) }
29
+ end
30
+
31
+ strip_empty(test_details)
32
+ end
33
+
34
+ def test_distribution
35
+ test_distribution_check
36
+ tests_arr = []
37
+ case @config[:test_distribution]
38
+ when "package"
39
+ test_data.each { |type| tests_arr << type[:package] }
40
+ when 'class', 'shard'
41
+ test_data.each { |type| tests_arr << "#{type[:package]}.#{type[:class]}" }
42
+ else
43
+ test_data.each do |type|
44
+ type[:tests].each { |test| tests_arr << "#{type[:package]}.#{type[:class]}##{test}" }
45
+ end
46
+ end
47
+ tests_arr.uniq
48
+ end
49
+
50
+ def test_distribution_check
51
+ return @config[:test_distribution] if @config[:test_distribution].kind_of?(Array)
52
+
53
+ distribution_types = %w[class testCase package shard]
54
+ unless distribution_types.include?(@config[:test_distribution]) || @config[:test_distribution].nil?
55
+ UI.user_error!("#{@config[:test_distribution]} is not a valid method of test distribution")
56
+ end
57
+ end
58
+
59
+ def strip_empty(test_details)
60
+ tests = []
61
+ test_details.each { |test| tests << test unless test[:tests].size.zero? }
62
+ tests
63
+ end
64
+
65
+ def tests_from(path)
66
+ stdout, = find(path, "@Test")
67
+ test_cases = []
68
+ stdout.split.each do |line|
69
+ test_cases << line.match(TEST_FUNCTION_REGEX).to_s.gsub(/[()]/, "") if line =~ TEST_FUNCTION_REGEX
70
+ end
71
+ strip_skipped(path, test_cases)
72
+ end
73
+
74
+ def fetch_disabled_tests(path)
75
+ stdout, = find(path, "@Ignore")
76
+ test_cases = []
77
+ stdout.split.each do |line|
78
+ test_cases << line.match(TEST_FUNCTION_REGEX).to_s.gsub(/[()]/, "") if line =~ TEST_FUNCTION_REGEX
79
+ end
80
+ test_cases
81
+ end
82
+
83
+ def strip_skipped(path, tests)
84
+ enabled_ui_tests = []
85
+ skipped_tests = fetch_disabled_tests(path)
86
+ tests.each do |test|
87
+ enabled_ui_tests << test unless skipped_tests.include?(test)
88
+ end
89
+ enabled_ui_tests
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,27 @@
1
+ require "open3"
2
+
3
+ # utility module for helper functions
4
+ module FileUtils
5
+ CLASS_NAME_REGEX = /(Spec|Specs|Test|Tests)/.freeze
6
+ FILE_TYPE_REGEX = /(.swift|.kt|.java)/.freeze
7
+
8
+ def read_file(name)
9
+ raise "File not found: #{name}" unless File.exist?(name)
10
+
11
+ File.read(name).split
12
+ end
13
+
14
+ def search_retrieve_test_classes(path)
15
+ Find.find(path).select do |f|
16
+ File.file?(f) if File.basename(f) =~ CLASS_NAME_REGEX
17
+ end
18
+ end
19
+
20
+ def find(class_name, regex)
21
+ syscall("find '#{class_name}' -type f -exec grep -h -C2 '#{regex}' {} +")
22
+ end
23
+
24
+ def syscall(*cmd)
25
+ Open3.capture3(*cmd)
26
+ end
27
+ end
@@ -0,0 +1,51 @@
1
+ require 'fastlane_core/ui/ui'
2
+ require 'fastlane'
3
+ require 'open-uri'
4
+ require_relative 'file_utils'
5
+
6
+ module Fastlane
7
+ module Saucectl
8
+ #
9
+ # This class provides the functions required to install the saucectl binary
10
+ #
11
+ class Installer
12
+ include FileUtils
13
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
14
+
15
+ def install
16
+ timeout_in_seconds = 30
17
+ Timeout.timeout(timeout_in_seconds) do
18
+ download_saucectl_installer
19
+ execute_saucectl_installer
20
+ UI.success("✅ Successfully installed saucectl runner binary 🚀")
21
+ rescue OpenURI::HTTPError => e
22
+ response = e.io
23
+ UI.user_error!("❌ Failed to install saucectl binary: status #{response.status[0]}")
24
+ end
25
+ end
26
+
27
+ def download_saucectl_installer
28
+ URI.open('sauce', 'wb') do |file|
29
+ file << URI.open('https://saucelabs.github.io/saucectl/install').read
30
+ end
31
+ end
32
+
33
+ def execute_saucectl_installer
34
+ status = system('sh sauce')
35
+ status == 1 ? UI.user_error!("❌ failed to install saucectl: #{stderr}") : status
36
+ executable = 'saucectl'
37
+ FileUtils.mv("bin/#{executable}", executable) unless File.exist?(executable)
38
+ end
39
+
40
+ def system(*cmd)
41
+ Open3.popen2e(*cmd) do |stdin, stdout_stderr, wait_thread|
42
+ Thread.new do
43
+ stdout_stderr.each { |out| UI.message(out) }
44
+ end
45
+ stdin.close
46
+ wait_thread.value
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,36 @@
1
+ require 'fastlane'
2
+ require 'open3'
3
+ require 'fastlane_core/ui/ui'
4
+ require_relative "file_utils"
5
+
6
+ module Fastlane
7
+ module Saucectl
8
+ #
9
+ # This class provides the ability to execute tests via configured specifications and capture the output of the sauce executable.
10
+ #
11
+ class Runner
12
+ include FileUtils
13
+ EXECUTABLE = 'saucectl'
14
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
15
+
16
+ def execute
17
+ unless File.exist?(EXECUTABLE)
18
+ UI.user_error!("❌ sauce labs executable file does not exist! Expected sauce executable file to be located at:'#{Dir.pwd}/#{EXECUTABLE}'")
19
+ end
20
+
21
+ system("chmod +x #{EXECUTABLE}")
22
+ system("./#{EXECUTABLE} run")
23
+ end
24
+
25
+ def system(*cmd)
26
+ Open3.popen2e(*cmd) do |stdin, stdout_stderr, wait_thread|
27
+ Thread.new do
28
+ stdout_stderr.each { |out| UI.message(out) }
29
+ end
30
+ stdin.close
31
+ wait_thread.value
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,46 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+ require_relative 'api'
5
+
6
+ module Fastlane
7
+ module Saucectl
8
+ # This class provides the ability to store, delete, and retrieve data from the Sauce Labs Storage API
9
+ class Storage
10
+ def initialize(config)
11
+ @config = config
12
+ end
13
+
14
+ # Get App Storage Files
15
+ # @return the set of files that have been uploaded to Sauce Storage by the requester.
16
+ def retrieve_all_apps
17
+ api = Fastlane::Saucectl::Api.new(@config)
18
+ api.retrieve_all_apps
19
+ end
20
+
21
+ # Delete app by the Sauce Labs identifier of the stored file. You can look up file IDs using the Get App Storage Files endpoint.
22
+ # https://docs.saucelabs.com/dev/api/storage/#get-app-storage-files
23
+ # @return json response containing the file id and the number of files deleted.
24
+ def delete_app_with_file_id
25
+ api = Fastlane::Saucectl::Api.new(@config)
26
+ api.delete_app("v1/storage/files/#{@config[:app_id]}")
27
+ end
28
+
29
+ # Deletes the specified group of files from Sauce Storage.
30
+ # The Sauce Labs identifier of the group of files. You can look up file IDs using the Get App Storage Groups endpoint.
31
+ # https://docs.saucelabs.com/dev/api/storage/#get-app-storage-groups
32
+ # @return json response containing the group ID and the number of files deleted.
33
+ def delete_all_apps_for_group_id
34
+ path = "v1/storage/files/#{@config[:group_id]}"
35
+ api = Fastlane::Saucectl::Api.new(@config)
36
+ api.delete_app(path)
37
+ end
38
+
39
+ # Uploads an application file to Sauce Storage for the purpose of mobile application testing
40
+ # @return a unique file ID assigned to the app.
41
+ def upload_app
42
+ Fastlane::Saucectl::Api.new(@config).upload
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,201 @@
1
+ require 'base64'
2
+ require 'fastlane'
3
+ require 'fastlane_core/ui/ui'
4
+ require_relative 'espresso'
5
+ require_relative 'xctest'
6
+
7
+ module Fastlane
8
+ module Saucectl
9
+ #
10
+ # This class will create test suites based on user specified configuration properties
11
+ #
12
+ class Suites
13
+ include FileUtils
14
+
15
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
16
+
17
+ def initialize(config)
18
+ @config = config
19
+ end
20
+
21
+ def create_test_plan
22
+ check_kind
23
+ if @config[:platform].casecmp('ios').zero?
24
+ is_ios_reqs_satisfied?
25
+ Fastlane::Saucectl::XCTest.new(@config)
26
+ else
27
+ Fastlane::Saucectl::Espresso.new(@config)
28
+ end
29
+ end
30
+
31
+ def check_kind
32
+ if @config[:platform].eql?('android')
33
+ UI.user_error!("❌ #{@config[:kind]} is not a supported test framework for android. Use espresso") unless @config[:kind].eql?('espresso')
34
+ else
35
+ UI.user_error!("❌ #{@config[:kind]} is not a supported test framework for iOS. Use xcuitest") unless @config[:kind].eql?('xcuitest')
36
+ end
37
+ end
38
+
39
+ def is_ios_reqs_satisfied?
40
+ if @config[:test_target].nil? && @config[:test_plan].nil?
41
+ UI.user_error!("❌ For ios you must specify test_target or test_plan")
42
+ end
43
+ end
44
+
45
+ def test_distribution_array
46
+ @config[:test_class] || create_test_plan.test_distribution
47
+ end
48
+
49
+ def suite_name(test_type)
50
+ if ENV['JOB_NAME'].nil? && ENV['BUILD_NUMBER'].nil?
51
+ "#{@config[:kind]}-#{test_type.split('.')[-1]}"
52
+ else
53
+ "#{ENV['JOB_NAME']}-#{ENV['BUILD_NUMBER']}-#{test_type.split('.')[-1]}"
54
+ end
55
+ end
56
+
57
+ def create_virtual_device_suites
58
+ if @config[:test_distribution] == 'shard'
59
+ shard_virtual_device_suites
60
+ elsif @config[:test_class]
61
+ custom_test_classes
62
+ else
63
+ test_suites = []
64
+ @config[:emulators].each do |emulator|
65
+ test_distribution_array.each do |test_type|
66
+ test_suites << {
67
+ 'name' => suite_name(test_type).downcase,
68
+ 'testOptions' => default_test_options(test_type)
69
+ }.merge(virtual_device_options(emulator))
70
+ end
71
+ end
72
+ test_suites
73
+ end
74
+ end
75
+
76
+ def shard_virtual_device_suites
77
+ UI.user_error!("❌ Cannot split #{@config[:test_distribution]}'s across virtual devices with a single emulator. \nPlease specify a minimum of two devices!") if @config[:emulators].size.eql?(1)
78
+ test_suites = []
79
+ arr = test_distribution_array
80
+ shards = arr.each_slice((arr.size / @config[:emulators].size.to_f).round).to_a
81
+ shards.each_with_index do |suite, i|
82
+ test_suites << {
83
+ 'name' => suite_name("shard #{i + 1}").downcase,
84
+ 'testOptions' => default_test_options(suite)
85
+ }.merge(virtual_device_options(@config[:emulators][i]))
86
+ end
87
+ test_suites
88
+ end
89
+
90
+ def shard_real_device_suites
91
+ test_suites = []
92
+ arr = test_distribution_array
93
+ shards = arr.each_slice((arr.size / @config[:devices].size.to_f).round).to_a
94
+ shards.each_with_index do |suite, i|
95
+ test_suites << {
96
+ 'name' => suite_name("shard #{i + 1}").downcase,
97
+ 'testOptions' => default_test_options(suite)
98
+ }.merge(real_device_options(@config[:devices][i]))
99
+ end
100
+ test_suites
101
+ end
102
+
103
+ def test_plan_suites
104
+ test_suites = []
105
+ @config[:devices].each do |device|
106
+ test_suites << {
107
+ 'name' => suite_name(@config[:test_plan].to_s).downcase,
108
+ 'testOptions' => default_test_options(test_distribution_array)
109
+ }.merge(real_device_options(device))
110
+ end
111
+ test_suites
112
+ end
113
+
114
+ def custom_test_classes
115
+ test_suites = []
116
+ devices = @config[:devices].nil? ? @config[:emulators] : @config[:devices]
117
+ devices.each do |device|
118
+ device_options = @config[:devices].nil? ? virtual_device_options(device) : real_device_options(device)
119
+ test_classes = @config[:test_class].reject(&:empty?).join(',')
120
+ test_suites << {
121
+ 'name' => suite_name(device[:name]).downcase,
122
+ 'testOptions' => default_test_options(test_classes.split(','))
123
+ }.merge(device_options)
124
+ end
125
+ test_suites
126
+ end
127
+
128
+ def create_real_device_suites
129
+ if !@config[:test_plan].nil? && @config[:test_distribution].eql?('class')
130
+ test_plan_suites
131
+ elsif @config[:test_distribution] == 'shard'
132
+ shard_real_device_suites
133
+ elsif @config[:test_class].kind_of?(Array)
134
+ custom_test_classes
135
+ else
136
+ test_suites = []
137
+ @config[:devices].each do |device|
138
+ test_distribution_array.each do |test_type|
139
+ test_suites << {
140
+ 'name' => suite_name(test_type).downcase,
141
+ 'testOptions' => default_test_options(test_type)
142
+ }.merge(real_device_options(device))
143
+ end
144
+ end
145
+ test_suites
146
+ end
147
+ end
148
+
149
+ def virtual_device_options(device)
150
+ platform_versions = device[:platform_versions].reject(&:empty?).join(',')
151
+ { 'emulators' => [{ 'name' => device[:name],
152
+ 'orientation' => device[:orientation],
153
+ 'platformVersions' => platform_versions.split(',') }] }
154
+ end
155
+
156
+ def real_device_options(device)
157
+ { 'devices' => [rdc_options(device)] }
158
+ end
159
+
160
+ def rdc_options(device)
161
+ device_type_key = device.key?(:id) ? 'id' : 'name'
162
+ name = device.key?(:id) ? device[:id] : device[:name]
163
+
164
+ base_device_hash = {
165
+ device_type_key => name,
166
+ 'orientation' => device[:orientation]
167
+ }.merge('options' => device_options(device))
168
+
169
+ unless device[:platform_version].nil?
170
+ base_device_hash = base_device_hash.merge({ 'platformVersion' => device[:platform_version] })
171
+ end
172
+
173
+ base_device_hash
174
+ end
175
+
176
+ def device_options(device)
177
+ {
178
+ 'carrierConnectivity' => device[:carrier_connectivity],
179
+ 'deviceType' => device[:device_type].upcase!,
180
+ 'private' => device[:private]
181
+ }
182
+ end
183
+
184
+ def default_test_options(test_type)
185
+ test_option_type = @config[:test_distribution].eql?('package') ? 'package' : 'class'
186
+ if @config[:platform] == 'android'
187
+ { test_option_type => test_type }.merge(android_test_options)
188
+ else
189
+ { 'class' => test_type }
190
+ end
191
+ end
192
+
193
+ def android_test_options
194
+ {
195
+ 'clearPackageData' => @config[:clear_data],
196
+ 'useTestOrchestrator' => @config[:use_test_orchestrator]
197
+ }
198
+ end
199
+ end
200
+ end
201
+ end