xamarin-test-cloud-appium 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +12 -0
- data/bin/test-cloud-appium +19 -0
- data/lib/xamarin-test-cloud-appium/cli.rb +919 -0
- data/lib/xamarin-test-cloud-appium/retriable_options.rb +27 -0
- data/lib/xamarin-test-cloud-appium/version.rb +3 -0
- data/test/ipa/features/step_definitions/calabash_steps.rb +1 -0
- data/test/ipa/features/step_definitions/my_first_steps.rb +4 -0
- data/test/ipa/features/support/env.rb +1 -0
- data/test/ipa/features/support/hooks.rb +0 -0
- data/test/ipa/features/support/launch.rb +77 -0
- data/test/test_parser.rb +72 -0
- metadata +315 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5e47054ddff2193bf58d5f1353024cfac18a5895
|
4
|
+
data.tar.gz: fd14d358bf04b33b366b6aecdddcd3d7c5bdfe6e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c636bbce1fed892c470c08724ade4926404476e928bb1c891043e1c11990566107d475f04831bda05ab2674691bfc5f1f071efa38a0c4a5715db68d35aa5a5e4
|
7
|
+
data.tar.gz: fbe7d9b46904cf72c549157410c67984021e4adc52ba6bbb429d19cb55ae975ae21dfd2d16a5057394d823f6d00d743cfe0d50a4ebe8f4666850006f641d95b4
|
data/README.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'xamarin-test-cloud-appium/cli'
|
3
|
+
require 'xamarin-test-cloud-appium/version'
|
4
|
+
|
5
|
+
begin
|
6
|
+
ENV['THOR_DEBUG'] = '1'
|
7
|
+
XamarinTestCloud::CLI.start
|
8
|
+
exit(true)
|
9
|
+
rescue XamarinTestCloud::ValidationError, Thor::RequiredArgumentMissingError, Thor::UndefinedCommandError => e
|
10
|
+
puts e.message
|
11
|
+
exit 64
|
12
|
+
rescue Thor::Error => e
|
13
|
+
puts e.message
|
14
|
+
if ENV['DEBUG'] == '1'
|
15
|
+
puts e.backtrace.join("\n") if e.backtrace
|
16
|
+
p e.class
|
17
|
+
end
|
18
|
+
exit 70
|
19
|
+
end
|
@@ -0,0 +1,919 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'yaml'
|
3
|
+
require 'erb'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'zip'
|
6
|
+
require 'digest'
|
7
|
+
require 'rest_client'
|
8
|
+
require 'json'
|
9
|
+
require 'rbconfig'
|
10
|
+
require 'tmpdir'
|
11
|
+
require 'fileutils'
|
12
|
+
require 'retriable'
|
13
|
+
require 'xamarin-test-cloud-appium/version'
|
14
|
+
require 'xamarin-test-cloud-appium/retriable_options'
|
15
|
+
require 'securerandom'
|
16
|
+
require 'open3'
|
17
|
+
|
18
|
+
|
19
|
+
trap "SIGINT" do
|
20
|
+
puts "Exiting"
|
21
|
+
exit 10
|
22
|
+
end
|
23
|
+
|
24
|
+
module XamarinTestCloud
|
25
|
+
|
26
|
+
class ValidationError < Thor::InvocationError
|
27
|
+
end
|
28
|
+
|
29
|
+
class CLI < Thor
|
30
|
+
include Thor::Actions
|
31
|
+
|
32
|
+
attr_accessor :app, :api_key, :appname, :test_parameters, :user,
|
33
|
+
:workspace, :config, :profile, :skip_config_check, :dry_run,
|
34
|
+
:device_selection, :pretty, :async, :async_json, :priority, :endpoint_path,
|
35
|
+
:locale, :series,
|
36
|
+
:dsym, :session_id, :appium
|
37
|
+
|
38
|
+
def self.exit_on_failure?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(*args)
|
43
|
+
self.session_id = SecureRandom.hex
|
44
|
+
begin
|
45
|
+
r = JSON.parse(http_post("check_version", {args: ARGV}))
|
46
|
+
if r["error_message"]
|
47
|
+
puts r["error_message"]
|
48
|
+
exit 1
|
49
|
+
end
|
50
|
+
rescue
|
51
|
+
end
|
52
|
+
super(*args)
|
53
|
+
end
|
54
|
+
|
55
|
+
FILE_UPLOAD_ENDPOINT = 'upload2'
|
56
|
+
FORM_URL_ENCODED_ENDPOINT = 'upload'
|
57
|
+
|
58
|
+
|
59
|
+
def self.source_root
|
60
|
+
File.join(File.dirname(__FILE__), '..')
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'version', 'Prints version of the xamarin-test-cloud gem'
|
64
|
+
|
65
|
+
def version
|
66
|
+
puts XamarinTestCloud::VERSION
|
67
|
+
end
|
68
|
+
|
69
|
+
desc 'submit <APP> <API_KEY>', 'Submits your app and test suite to Xamarin Test Cloud'
|
70
|
+
|
71
|
+
|
72
|
+
method_option 'app-name',
|
73
|
+
:desc => 'App name to create or add test to',
|
74
|
+
:aliases => '-a',
|
75
|
+
:required => false,
|
76
|
+
:type => :string
|
77
|
+
|
78
|
+
method_option 'devices',
|
79
|
+
:desc => 'Device selection',
|
80
|
+
:aliases => '-d',
|
81
|
+
:required => true,
|
82
|
+
:type => :string
|
83
|
+
|
84
|
+
method_option 'test-parameters',
|
85
|
+
:desc => 'Test parameters (e.g., -params username:nat@xamarin.com password:xamarin)',
|
86
|
+
:aliases => '-params',
|
87
|
+
:type => :hash
|
88
|
+
|
89
|
+
method_option :workspace,
|
90
|
+
:desc => 'Path to your Calabash workspace (containing your features folder)',
|
91
|
+
:aliases => '-w',
|
92
|
+
:type => :string
|
93
|
+
|
94
|
+
method_option :config,
|
95
|
+
:desc => 'Cucumber configuration file (cucumber.yml)',
|
96
|
+
:aliases => '-c',
|
97
|
+
:type => :string
|
98
|
+
|
99
|
+
method_option 'skip-config-check',
|
100
|
+
:desc => "Force running without Cucumber profile (cucumber.yml)",
|
101
|
+
:type => :boolean,
|
102
|
+
:default => false
|
103
|
+
|
104
|
+
method_option :profile,
|
105
|
+
:desc => 'Profile to run (profile from cucumber.yml)',
|
106
|
+
:aliases => '-p',
|
107
|
+
:type => :string
|
108
|
+
|
109
|
+
method_option :pretty,
|
110
|
+
:desc => 'Pretty print output.',
|
111
|
+
:type => :boolean,
|
112
|
+
:default => false
|
113
|
+
|
114
|
+
method_option :async,
|
115
|
+
:desc => "Don't block waiting for test results.",
|
116
|
+
:type => :boolean,
|
117
|
+
:default => false
|
118
|
+
|
119
|
+
method_option 'async-json',
|
120
|
+
:desc => "Don't block waiting for test results. Output results in json format.",
|
121
|
+
:type => :boolean,
|
122
|
+
:default => false
|
123
|
+
|
124
|
+
method_option :priority,
|
125
|
+
:desc => "REMOVED. You should not pass this option.",
|
126
|
+
:type => :boolean,
|
127
|
+
:default => false
|
128
|
+
|
129
|
+
method_option 'dry-run',
|
130
|
+
:desc => "Sanity check only, don't upload",
|
131
|
+
:aliases => '-s',
|
132
|
+
:type => :boolean,
|
133
|
+
:default => false #do upload by default
|
134
|
+
|
135
|
+
method_option 'locale',
|
136
|
+
:desc => "System language",
|
137
|
+
:type => :string
|
138
|
+
|
139
|
+
method_option 'series',
|
140
|
+
:desc => "Test series",
|
141
|
+
:type => :string
|
142
|
+
|
143
|
+
method_option 'dsym-file',
|
144
|
+
:desc => 'Optional dSym file for iOS Crash symbolication',
|
145
|
+
:aliases => '-y',
|
146
|
+
:required => false,
|
147
|
+
:type => :string
|
148
|
+
|
149
|
+
method_option 'user',
|
150
|
+
:desc => 'Email address of the user uploading',
|
151
|
+
:aliases => '-u',
|
152
|
+
:required => false,
|
153
|
+
:type => :string
|
154
|
+
|
155
|
+
def submit(app, api_key)
|
156
|
+
|
157
|
+
self.pretty = options[:pretty]
|
158
|
+
self.async_json = options['async-json']
|
159
|
+
self.async = options[:async] || self.async_json
|
160
|
+
|
161
|
+
# Async mode wraps all console output in a json object
|
162
|
+
# So we need to intercept all writes to $stdout
|
163
|
+
if self.async_json
|
164
|
+
@async_log = StringIO.new
|
165
|
+
@async_result = {
|
166
|
+
test_run_id: nil,
|
167
|
+
error_messages: [],
|
168
|
+
log: []
|
169
|
+
}
|
170
|
+
$stdout = @async_log
|
171
|
+
end
|
172
|
+
|
173
|
+
app_path = File.expand_path(app)
|
174
|
+
unless File.exist?(app_path)
|
175
|
+
raise ValidationError, "App is not a file: #{app_path}"
|
176
|
+
end
|
177
|
+
|
178
|
+
if shared_runtime?(app_path)
|
179
|
+
puts "Xamarin Test Cloud doesn't yet support shared runtime apps."
|
180
|
+
puts "To test your app it needs to be compiled for release."
|
181
|
+
puts "You can learn how to compile you app for release here:"
|
182
|
+
puts "http://docs.xamarin.com/guides/android/deployment%2C_testing%2C_and_metrics/publishing_an_application/part_1_-_preparing_an_application_for_release"
|
183
|
+
raise ValidationError, "Aborting"
|
184
|
+
end
|
185
|
+
|
186
|
+
app_extension = File.extname(app_path)
|
187
|
+
unless /ipa/i.match(app_extension) || /apk/i.match(app_extension)
|
188
|
+
raise ValidationError, "App #{app_path} must be an .ipa or .apk file"
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
self.app = app_path
|
193
|
+
|
194
|
+
self.user = options['user']
|
195
|
+
|
196
|
+
self.dry_run = options['dry-run']
|
197
|
+
|
198
|
+
self.api_key = api_key
|
199
|
+
|
200
|
+
self.test_parameters = options['test-parameters'] || {}
|
201
|
+
|
202
|
+
self.appname = options['app-name']
|
203
|
+
|
204
|
+
self.device_selection = options['devices']
|
205
|
+
|
206
|
+
device_selection_data = validate_device_selection
|
207
|
+
|
208
|
+
self.locale = options['locale']
|
209
|
+
|
210
|
+
self.series = options['series']
|
211
|
+
|
212
|
+
self.priority = options['priority']
|
213
|
+
|
214
|
+
self.skip_config_check = options['skip-config-check']
|
215
|
+
|
216
|
+
self.dsym = options['dsym-file']
|
217
|
+
|
218
|
+
self.appium = true
|
219
|
+
|
220
|
+
if dsym
|
221
|
+
dsym_extension = File.extname(self.dsym)
|
222
|
+
unless /dsym/i.match(dsym_extension) && File.directory?(dsym)
|
223
|
+
raise ValidationError, "dsym-file must be a directory and have dSYM extension: #{dsym}"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
workspace_path = options[:workspace] || File.expand_path('.')
|
228
|
+
|
229
|
+
unless File.directory?(workspace_path)
|
230
|
+
raise ValidationError, "Provided workspace: #{workspace_path} is not a directory."
|
231
|
+
end
|
232
|
+
|
233
|
+
workspace_basename = File.basename(workspace_path)
|
234
|
+
if workspace_basename.downcase == 'features'
|
235
|
+
self.workspace = File.expand_path(File.join(workspace_path, '..'))
|
236
|
+
puts "Deriving workspace #{self.workspace} from features folder #{workspace_basename}"
|
237
|
+
else
|
238
|
+
self.workspace = File.expand_path(workspace_path)
|
239
|
+
end
|
240
|
+
|
241
|
+
self.workspace = File.join(self.workspace, File::Separator)
|
242
|
+
|
243
|
+
|
244
|
+
unless appium || File.directory?(File.join(self.workspace, 'features'))
|
245
|
+
log_header "Did not find features folder in workspace #{self.workspace}"
|
246
|
+
puts "Either run the test-cloud command from the directory containing your features"
|
247
|
+
puts "or use the --workspace option to refer to this directory"
|
248
|
+
puts "See also test-cloud help submit"
|
249
|
+
raise ValidationError, "Unable to find features folder in #{self.workspace}"
|
250
|
+
end
|
251
|
+
|
252
|
+
parse_and_set_config_and_profile
|
253
|
+
unless self.skip_config_check
|
254
|
+
default_config = File.join(self.workspace, 'config', 'cucumber.yml')
|
255
|
+
if File.exist?(default_config) && self.config.nil?
|
256
|
+
log_header 'Warning: Detected cucumber.yml config file, but no --config specified'
|
257
|
+
puts "Please specify --config #{default_config}"
|
258
|
+
puts 'and specify a profile via --profile'
|
259
|
+
puts 'If you know what you are doing you can skip this check with'
|
260
|
+
puts '--skip-config-check'
|
261
|
+
raise ValidationError, "#{default_config} detected but no profile selected."
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
if debug?
|
266
|
+
puts "Host = #{self.host}"
|
267
|
+
puts "User = #{self.user}"
|
268
|
+
puts "App = #{self.app}"
|
269
|
+
puts "App Name = #{self.app}"
|
270
|
+
puts "TestParams = #{self.test_parameters}"
|
271
|
+
puts "API Key = #{self.api_key}"
|
272
|
+
puts "Device Selection = #{self.device_selection}"
|
273
|
+
puts "Workspace = #{self.workspace}"
|
274
|
+
puts "Config = #{self.config}"
|
275
|
+
puts "Profile = #{self.profile}"
|
276
|
+
puts "dSym = #{self.dsym}"
|
277
|
+
end
|
278
|
+
|
279
|
+
|
280
|
+
#Argument parsing done
|
281
|
+
|
282
|
+
test_jon_data = submit_test_job(device_selection_data)
|
283
|
+
if self.dry_run
|
284
|
+
return
|
285
|
+
end
|
286
|
+
json = test_jon_data[:body]
|
287
|
+
if debug?
|
288
|
+
p json
|
289
|
+
end
|
290
|
+
|
291
|
+
log_header('Test enqueued')
|
292
|
+
puts "User: #{json['user_email']}"
|
293
|
+
puts "Team: #{json['team']}" if json['team']
|
294
|
+
|
295
|
+
|
296
|
+
rejected_devices = json['rejected_devices']
|
297
|
+
if rejected_devices && rejected_devices.size > 0
|
298
|
+
puts 'Skipping devices (you can update your selections via https://testcloud.xamarin.com)'
|
299
|
+
rejected_devices.each { |d| puts d }
|
300
|
+
end
|
301
|
+
puts ''
|
302
|
+
|
303
|
+
puts 'Running on Devices:'
|
304
|
+
json['devices'].each do |device|
|
305
|
+
puts device
|
306
|
+
end
|
307
|
+
puts ''
|
308
|
+
|
309
|
+
|
310
|
+
unless self.async
|
311
|
+
wait_for_job(json['id'])
|
312
|
+
else
|
313
|
+
log 'Async mode: not awaiting test results'
|
314
|
+
@async_result[:test_run_id] = json['id'] if self.async_json
|
315
|
+
end
|
316
|
+
|
317
|
+
rescue XamarinTestCloud::ValidationError => e
|
318
|
+
if self.async_json
|
319
|
+
@async_result[:error_messages] << e.message
|
320
|
+
else
|
321
|
+
raise
|
322
|
+
end
|
323
|
+
|
324
|
+
ensure
|
325
|
+
$stdout = STDOUT
|
326
|
+
if self.async_json
|
327
|
+
process_async_log
|
328
|
+
puts @async_result.to_json
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
|
333
|
+
default_task :submit
|
334
|
+
|
335
|
+
no_tasks do
|
336
|
+
|
337
|
+
def debug?
|
338
|
+
ENV['DEBUG'] == '1'
|
339
|
+
end
|
340
|
+
|
341
|
+
def process_async_log
|
342
|
+
@async_result[:log] = @async_log.string
|
343
|
+
.split(/\n/).map { |string| string.gsub(/\e\[(\d+)m/, '').strip }
|
344
|
+
.select { |string| string.length > 0 }
|
345
|
+
end
|
346
|
+
|
347
|
+
def exit_on_failure?
|
348
|
+
true
|
349
|
+
end
|
350
|
+
|
351
|
+
def wait_for_job(id)
|
352
|
+
retry_opts = XamarinTestCloud::RetriableOptions.tries_and_interval(60, 10)
|
353
|
+
while(true)
|
354
|
+
status_json = Retriable.retriable(retry_opts) do
|
355
|
+
JSON.parse(http_post("status_v3", {'id' => id, 'api_key' => api_key, 'user' => user}))
|
356
|
+
end
|
357
|
+
|
358
|
+
if debug?
|
359
|
+
log "Status JSON result:"
|
360
|
+
puts status_json
|
361
|
+
end
|
362
|
+
|
363
|
+
wait_time = (Integer status_json["wait_time"] rescue nil) || 10
|
364
|
+
wait_time = 1 if debug?
|
365
|
+
log status_json["message"]
|
366
|
+
|
367
|
+
if status_json["exit_code"]
|
368
|
+
exit Integer status_json["exit_code"]
|
369
|
+
end
|
370
|
+
|
371
|
+
sleep wait_time
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def validate_device_selection
|
376
|
+
return device_selection if device_selection == device_selection.upcase #Allow for special device selections to be passed to the server. Needs to been in all caps.
|
377
|
+
unless /^[0-9a-fA-F]{8,12}$/ =~ device_selection
|
378
|
+
raise ValidationError, 'Device selection is not in the proper format. Please generate a new one on the Xamarin Test Cloud website.'
|
379
|
+
end
|
380
|
+
device_selection
|
381
|
+
end
|
382
|
+
|
383
|
+
def workspace_gemfile
|
384
|
+
File.join(self.workspace, 'Gemfile')
|
385
|
+
end
|
386
|
+
|
387
|
+
def workspace_gemfile_lock
|
388
|
+
File.join(self.workspace, 'Gemfile.lock')
|
389
|
+
end
|
390
|
+
|
391
|
+
def submit_test_job(device_selection_data)
|
392
|
+
tmpdir = Dir.mktmpdir
|
393
|
+
if debug?
|
394
|
+
log "Packaging gems in: #{tmpdir}"
|
395
|
+
end
|
396
|
+
start_at = Time.now
|
397
|
+
|
398
|
+
server = verify_app_and_extract_test_server
|
399
|
+
|
400
|
+
unless appium
|
401
|
+
log_header('Checking for Gemfile')
|
402
|
+
|
403
|
+
if File.exist?(workspace_gemfile)
|
404
|
+
FileUtils.cp workspace_gemfile, tmpdir
|
405
|
+
FileUtils.cp workspace_gemfile_lock, tmpdir if File.exist?(workspace_gemfile_lock)
|
406
|
+
else
|
407
|
+
copy_default_gemfile(File.join(tmpdir, "Gemfile"), server)
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
log_header('Packaging')
|
412
|
+
|
413
|
+
unless appium
|
414
|
+
ENV['BUNDLE_GEMFILE'] = File.join(tmpdir, "Gemfile")
|
415
|
+
FileUtils.cd(self.workspace) do
|
416
|
+
if self.async_json
|
417
|
+
bundle_log, status = Open3.capture2e('bundle package --all')
|
418
|
+
puts bundle_log
|
419
|
+
else
|
420
|
+
system('bundle package --all')
|
421
|
+
status = $?
|
422
|
+
end
|
423
|
+
if status != 0
|
424
|
+
log_and_abort 'Bundler failed. Please check command: bundle package'
|
425
|
+
end
|
426
|
+
end
|
427
|
+
log_header('Verifying dependencies')
|
428
|
+
verify_dependencies(tmpdir)
|
429
|
+
|
430
|
+
if dry_run
|
431
|
+
log_header('Dry run only')
|
432
|
+
log('Dry run completed OK')
|
433
|
+
return
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
app_file, dsym_zip, files, paths = gather_files_and_paths_to_upload(all_files(tmpdir), tmpdir)
|
438
|
+
|
439
|
+
log_header('Uploading negotiated files')
|
440
|
+
|
441
|
+
upload_data = {'files' => files,
|
442
|
+
'paths' => paths,
|
443
|
+
'user' => self.user,
|
444
|
+
'client_version' => XamarinTestCloud::VERSION,
|
445
|
+
'app_file' => app_file,
|
446
|
+
'device_selection' => device_selection_data,
|
447
|
+
'app' => self.appname,
|
448
|
+
'test_parameters' => self.test_parameters,
|
449
|
+
'locale' => self.locale,
|
450
|
+
'appium' => self.appium,
|
451
|
+
'series' => self.series,
|
452
|
+
'api_key' => api_key,
|
453
|
+
'dsym_file' => dsym_zip,
|
454
|
+
'dsym_filename' => dsym_file_name,
|
455
|
+
'app_filename' => File.basename(app)}
|
456
|
+
|
457
|
+
if profile #only if config and profile
|
458
|
+
upload_data['profile'] = profile
|
459
|
+
end
|
460
|
+
|
461
|
+
if debug?
|
462
|
+
puts JSON.pretty_generate(upload_data)
|
463
|
+
end
|
464
|
+
|
465
|
+
|
466
|
+
contains_file = files.find { |f| f.is_a?(File) }
|
467
|
+
|
468
|
+
contains_file = contains_file || app_file.is_a?(File)
|
469
|
+
|
470
|
+
if contains_file
|
471
|
+
self.endpoint_path = FILE_UPLOAD_ENDPOINT #nginx receives upload
|
472
|
+
else
|
473
|
+
self.endpoint_path = FORM_URL_ENCODED_ENDPOINT #ruby receives upload
|
474
|
+
end
|
475
|
+
|
476
|
+
if debug?
|
477
|
+
puts "Will upload to file path: #{self.endpoint_path}"
|
478
|
+
end
|
479
|
+
|
480
|
+
|
481
|
+
response = http_post(endpoint_path, upload_data) do |response, request, result, &block|
|
482
|
+
if debug?
|
483
|
+
puts "Request url: #{request.url}"
|
484
|
+
puts "Response code: #{response.code}"
|
485
|
+
puts "Response body: #{response.body}"
|
486
|
+
end
|
487
|
+
case response.code
|
488
|
+
when 200..202
|
489
|
+
response
|
490
|
+
when 400
|
491
|
+
error_message = JSON.parse(response.body)['error_message'] rescue 'Bad request'
|
492
|
+
log_and_abort(error_message)
|
493
|
+
when 403
|
494
|
+
error_message = JSON.parse(response.body)['message'] rescue 'Forbidden'
|
495
|
+
log_and_abort(error_message)
|
496
|
+
when 413
|
497
|
+
error_message = 'Files are too large'
|
498
|
+
log_and_abort(error_message)
|
499
|
+
else
|
500
|
+
error_message = 'Unexpected Error. Please contact support at testcloud@xamarin.com.'
|
501
|
+
log_and_abort(error_message)
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
return :status => response.code, :body => JSON.parse(response)
|
506
|
+
|
507
|
+
end
|
508
|
+
|
509
|
+
|
510
|
+
def copy_default_gemfile(gemfile_path, server)
|
511
|
+
log('')
|
512
|
+
log('Gemfile missing.')
|
513
|
+
log('You must provide a Gemfile in your workspace.')
|
514
|
+
log('A Gemfile must describe your dependencies and their versions')
|
515
|
+
log('See: http://gembundler.com/v1.3/gemfile.html')
|
516
|
+
log('')
|
517
|
+
log('Warning proceeding with default Gemfile.')
|
518
|
+
log('It is strongly recommended that you create a custom Gemfile.')
|
519
|
+
|
520
|
+
File.open(gemfile_path, "w") do |f|
|
521
|
+
f.puts "source 'http://rubygems.org'"
|
522
|
+
if is_android?
|
523
|
+
f.puts "gem 'calabash-android', '#{calabash_android_version}'"
|
524
|
+
elsif is_ios?
|
525
|
+
f.puts "gem 'calabash-cucumber', '#{calabash_ios_version}'"
|
526
|
+
else
|
527
|
+
raise ValidationError, 'Your app must be an ipa or apk file.'
|
528
|
+
end
|
529
|
+
end
|
530
|
+
log("Proceeding with Gemfile: #{gemfile_path}")
|
531
|
+
|
532
|
+
puts(File.read(gemfile_path))
|
533
|
+
|
534
|
+
log('')
|
535
|
+
end
|
536
|
+
|
537
|
+
def gather_files_and_paths_to_upload(collected_files, tmpdir)
|
538
|
+
|
539
|
+
log_header('Calculating digests')
|
540
|
+
|
541
|
+
file_paths = collected_files[:files]
|
542
|
+
feature_prefix = collected_files[:feature_prefix]
|
543
|
+
workspace_prefix = collected_files[:workspace_prefix]
|
544
|
+
|
545
|
+
hashes = file_paths.collect { |f| digest(f) }
|
546
|
+
|
547
|
+
if hashes.nil? || hashes.size == 0
|
548
|
+
hashes << '0222222222222222222222222222222222222222222222222222222222222222'
|
549
|
+
end
|
550
|
+
|
551
|
+
|
552
|
+
log_header('Negotiating upload')
|
553
|
+
|
554
|
+
app_digest = digest(app)
|
555
|
+
|
556
|
+
dsym_digest= nil
|
557
|
+
if dsym
|
558
|
+
FileUtils.cp_r(dsym, tmpdir)
|
559
|
+
files_in_dwarf = Dir.glob(File.join(tmpdir, File.basename(dsym), 'Contents', 'Resources', 'DWARF', '*'))
|
560
|
+
unless files_in_dwarf.count == 1
|
561
|
+
raise ValidationError, "dSym #{dsym} contains more than one file in Contents/Resources/DWARF: #{files_in_dwarf}"
|
562
|
+
end
|
563
|
+
|
564
|
+
dsym_abs_path= files_in_dwarf.first
|
565
|
+
dsym_digest = digest(dsym_abs_path)
|
566
|
+
end
|
567
|
+
out = {'hashes' => hashes, 'app_hash' => app_digest, 'dsym_hash' => dsym_digest}
|
568
|
+
|
569
|
+
response = http_post('check_hash', out)
|
570
|
+
|
571
|
+
|
572
|
+
cache_status = JSON.parse(response)
|
573
|
+
|
574
|
+
log_header('Gathering files based on negotiation')
|
575
|
+
|
576
|
+
files = []
|
577
|
+
paths = []
|
578
|
+
file_paths.each do |file|
|
579
|
+
if cache_status[digest(file)]
|
580
|
+
#Server already knows about this file. No need to upload it.
|
581
|
+
files << digest(file)
|
582
|
+
else
|
583
|
+
#Upload file
|
584
|
+
files << File.new(file)
|
585
|
+
end
|
586
|
+
|
587
|
+
if file.start_with?(feature_prefix)
|
588
|
+
prefix = feature_prefix
|
589
|
+
else
|
590
|
+
prefix = workspace_prefix
|
591
|
+
end
|
592
|
+
paths << file.sub(prefix, '').sub("#{tmpdir}/", '')
|
593
|
+
end
|
594
|
+
|
595
|
+
if config
|
596
|
+
files << File.new(config)
|
597
|
+
paths << 'config/cucumber.yml'
|
598
|
+
end
|
599
|
+
|
600
|
+
app_file = cache_status[app_digest] ? app_digest : File.new(app)
|
601
|
+
|
602
|
+
if dsym_digest
|
603
|
+
dsym_file = cache_status[dsym_digest] ? dsym_digest : File.new(dsym_abs_path)
|
604
|
+
end
|
605
|
+
|
606
|
+
return app_file, dsym_file, files, paths
|
607
|
+
end
|
608
|
+
|
609
|
+
def digest(file)
|
610
|
+
Digest::SHA256.file(file).hexdigest
|
611
|
+
end
|
612
|
+
|
613
|
+
def unzip_file (file, destination)
|
614
|
+
Zip::File.open(file) { |zip_file|
|
615
|
+
zip_file.each { |f|
|
616
|
+
f_path=File.join(destination, f.name)
|
617
|
+
FileUtils.mkdir_p(File.dirname(f_path))
|
618
|
+
zip_file.extract(f, f_path) unless File.exist?(f_path)
|
619
|
+
}
|
620
|
+
}
|
621
|
+
end
|
622
|
+
|
623
|
+
def is_android?
|
624
|
+
app.end_with? '.apk'
|
625
|
+
end
|
626
|
+
|
627
|
+
def is_ios?
|
628
|
+
app.end_with? '.ipa'
|
629
|
+
end
|
630
|
+
|
631
|
+
def calabash_android_version
|
632
|
+
version = nil
|
633
|
+
|
634
|
+
|
635
|
+
if File.exist?(workspace_gemfile)
|
636
|
+
FileUtils.cd(self.workspace) do
|
637
|
+
version = `bundle exec ruby -e "require 'calabash-android/version'; puts Calabash::Android::VERSION"`
|
638
|
+
version = version && version.strip
|
639
|
+
end
|
640
|
+
end
|
641
|
+
unless version
|
642
|
+
require 'calabash-android'
|
643
|
+
version = Calabash::Android::VERSION
|
644
|
+
end
|
645
|
+
|
646
|
+
version = version.strip
|
647
|
+
end
|
648
|
+
|
649
|
+
def calabash_ios_version
|
650
|
+
version = nil
|
651
|
+
if File.exist?(workspace_gemfile)
|
652
|
+
FileUtils.cd(self.workspace) do
|
653
|
+
version = `bundle exec ruby -e "require 'calabash-cucumber/version'; puts Calabash::Cucumber::VERSION"`
|
654
|
+
version = version && version.strip
|
655
|
+
end
|
656
|
+
end
|
657
|
+
unless version
|
658
|
+
require 'calabash-cucumber'
|
659
|
+
version = Calabash::Cucumber::VERSION
|
660
|
+
end
|
661
|
+
version = version.strip
|
662
|
+
end
|
663
|
+
|
664
|
+
def test_server_path
|
665
|
+
require 'digest/md5'
|
666
|
+
digest = Digest::MD5.file(app).hexdigest
|
667
|
+
File.join(self.workspace, 'test_servers', "#{digest}_#{calabash_android_version}.apk")
|
668
|
+
end
|
669
|
+
|
670
|
+
def all_files(tmpdir)
|
671
|
+
dir = workspace
|
672
|
+
|
673
|
+
files = Dir.glob(File.join("#{dir}features", '**', '*'))
|
674
|
+
|
675
|
+
unless appium
|
676
|
+
if File.directory?("#{dir}playback")
|
677
|
+
files += Dir.glob(File.join("#{dir}playback", '*'))
|
678
|
+
end
|
679
|
+
end
|
680
|
+
|
681
|
+
if config
|
682
|
+
files << config
|
683
|
+
end
|
684
|
+
|
685
|
+
files += Dir.glob(File.join(tmpdir, "vendor", 'cache', '*'))
|
686
|
+
|
687
|
+
if workspace and workspace.strip != ''
|
688
|
+
if appium
|
689
|
+
files += Dir.glob(File.join("#{workspace}", "**", "*"))
|
690
|
+
else
|
691
|
+
files += Dir.glob("#{workspace}Gemfile")
|
692
|
+
files += Dir.glob("#{workspace}Gemfile.lock")
|
693
|
+
end
|
694
|
+
else
|
695
|
+
raise ValidationError, "For appium, a workspace folder containing your pom.xml, test-classes and dependency-jars must be provided" if appium
|
696
|
+
end
|
697
|
+
|
698
|
+
if is_android? && !appium
|
699
|
+
files << test_server_path
|
700
|
+
end
|
701
|
+
|
702
|
+
{:feature_prefix => dir, :workspace_prefix => workspace, :files => files.find_all { |file_or_dir| File.file? file_or_dir }}
|
703
|
+
end
|
704
|
+
|
705
|
+
def dsym_file_name
|
706
|
+
if dsym
|
707
|
+
"#{File.basename(self.app)}_dSym"
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
711
|
+
def host
|
712
|
+
ENV['XTC_ENDPOINT'] || 'https://testcloud.xamarin.com/ci'
|
713
|
+
end
|
714
|
+
|
715
|
+
def http_post(address, args = {}, &block)
|
716
|
+
args['uploader_version'] = XamarinTestCloud::VERSION
|
717
|
+
args['session_id'] = session_id
|
718
|
+
exec_options = {}
|
719
|
+
if ENV['XTC_USERNAME'] && ENV['XTC_PASSWORD']
|
720
|
+
exec_options[:user] = ENV['XTC_USERNAME']
|
721
|
+
exec_options[:password] = ENV['XTC_PASSWORD']
|
722
|
+
end
|
723
|
+
|
724
|
+
if block_given?
|
725
|
+
exec_options = exec_options.merge({:method => :post, :url => "#{host}/#{address}", :payload => args,
|
726
|
+
:timeout => 90000000,
|
727
|
+
:open_timeout => 15,
|
728
|
+
:headers => {:content_type => 'multipart/form-data'}})
|
729
|
+
response = RestClient::Request.execute(exec_options) do |response, request, result, &other_block|
|
730
|
+
block.call(response, request, result, &other_block)
|
731
|
+
end
|
732
|
+
else
|
733
|
+
exec_options = exec_options.merge(:method => :post, :url => "#{host}/#{address}", :payload => args)
|
734
|
+
response = RestClient::Request.execute(exec_options)
|
735
|
+
end
|
736
|
+
|
737
|
+
response.body
|
738
|
+
end
|
739
|
+
|
740
|
+
def is_windows?
|
741
|
+
(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
|
742
|
+
end
|
743
|
+
|
744
|
+
def is_macosx?
|
745
|
+
(RbConfig::CONFIG['host_os'] =~ /darwin/)
|
746
|
+
end
|
747
|
+
|
748
|
+
def validate_ipa(ipa)
|
749
|
+
result = false
|
750
|
+
dir = Dir.mktmpdir #do |dir|
|
751
|
+
|
752
|
+
unzip_file(ipa, dir)
|
753
|
+
unless File.directory?("#{dir}/Payload") #macos only
|
754
|
+
raise ValidationError, "Unzipping #{ipa} to #{dir} failed: Did not find a Payload directory (invalid .ipa)."
|
755
|
+
end
|
756
|
+
app_dir = Dir.foreach("#{dir}/Payload").find { |d| /\.app$/.match(d) }
|
757
|
+
res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/"* -o 2> /dev/null | grep CalabashServer`
|
758
|
+
|
759
|
+
if /CalabashServer/.match(res)
|
760
|
+
puts "ipa: #{ipa} *contains* calabash.framework"
|
761
|
+
result = :calabash
|
762
|
+
end
|
763
|
+
|
764
|
+
unless result
|
765
|
+
res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/"* -o 2> /dev/null | grep FrankServer`
|
766
|
+
if /FrankServer/.match(res)
|
767
|
+
puts "ipa: #{ipa} *contains* FrankServer"
|
768
|
+
raise ValidationError, 'Frank not supported just yet'
|
769
|
+
else
|
770
|
+
puts "ipa: #{ipa} *does not contain* calabash.framework"
|
771
|
+
result = false
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
result
|
776
|
+
end
|
777
|
+
|
778
|
+
|
779
|
+
def log(message)
|
780
|
+
if message.is_a? Array
|
781
|
+
message.each { |m| log(m) }
|
782
|
+
else
|
783
|
+
puts "#{Time.now } #{message}"
|
784
|
+
$stdout.flush
|
785
|
+
end
|
786
|
+
end
|
787
|
+
|
788
|
+
def log_header(message)
|
789
|
+
if is_windows?
|
790
|
+
puts "\n### #{message} ###"
|
791
|
+
else
|
792
|
+
puts "\n\e[#{35}m### #{message} ###\e[0m"
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
796
|
+
|
797
|
+
def verify_app_and_extract_test_server
|
798
|
+
server = nil
|
799
|
+
|
800
|
+
unless File.exist?(app)
|
801
|
+
raise ValidationError, "No such file: #{app}"
|
802
|
+
end
|
803
|
+
unless (/\.(apk|ipa)$/ =~ app)
|
804
|
+
raise ValidationError, '<APP> should be either an ipa or apk file.'
|
805
|
+
end
|
806
|
+
if is_ios? and is_macosx?
|
807
|
+
log_header('Checking ipa for linking with Calabash')
|
808
|
+
server = validate_ipa(app)
|
809
|
+
abort_unless(server) do
|
810
|
+
puts 'The .ipa file does not seem to be linked with Calabash.'
|
811
|
+
puts 'Verify that your app is linked correctly.'
|
812
|
+
end
|
813
|
+
end
|
814
|
+
server
|
815
|
+
end
|
816
|
+
|
817
|
+
def abort(&block)
|
818
|
+
yield block
|
819
|
+
exit 1
|
820
|
+
end
|
821
|
+
|
822
|
+
def abort_unless(condition, &block)
|
823
|
+
unless condition
|
824
|
+
yield block
|
825
|
+
exit 1
|
826
|
+
end
|
827
|
+
end
|
828
|
+
|
829
|
+
def log_and_abort(message)
|
830
|
+
raise XamarinTestCloud::ValidationError.new(message) if self.async_json
|
831
|
+
abort do
|
832
|
+
print 'Error: '
|
833
|
+
puts message
|
834
|
+
end
|
835
|
+
end
|
836
|
+
|
837
|
+
|
838
|
+
def shared_runtime?(app_path)
|
839
|
+
f = files(app_path)
|
840
|
+
f.any? do |file|
|
841
|
+
filename = file[:filename]
|
842
|
+
if filename.end_with?("libmonodroid.so")
|
843
|
+
file[:size] < 120 * 1024 && f.none? { |x| x[:filename] == filename.sub("libmonodroid.so", "libmonosgen-2.0.so") }
|
844
|
+
end
|
845
|
+
end
|
846
|
+
end
|
847
|
+
|
848
|
+
def files(app)
|
849
|
+
Zip::File.open(app) do |zip_file|
|
850
|
+
zip_file.collect do |entry|
|
851
|
+
{:filename => entry.to_s, :size => entry.size}
|
852
|
+
end
|
853
|
+
end
|
854
|
+
end
|
855
|
+
|
856
|
+
|
857
|
+
def verify_dependencies(path)
|
858
|
+
if is_android?
|
859
|
+
abort_unless(File.exist?(test_server_path)) do
|
860
|
+
puts 'No test server found. Please run:'
|
861
|
+
puts " calabash-android build #{app}"
|
862
|
+
end
|
863
|
+
calabash_gem = Dir.glob("#{path}/vendor/cache/calabash-android-*").first
|
864
|
+
abort_unless(calabash_gem) do
|
865
|
+
puts 'calabash-android was not packaged correct.'
|
866
|
+
puts 'Please tell testcloud@xamarin.com about this bug.'
|
867
|
+
end
|
868
|
+
end
|
869
|
+
end
|
870
|
+
|
871
|
+
|
872
|
+
def parse_and_set_config_and_profile
|
873
|
+
config_path = options[:config]
|
874
|
+
if config_path
|
875
|
+
config_path = File.expand_path(config_path)
|
876
|
+
unless File.exist?(config_path)
|
877
|
+
raise ValidationError, "Config file does not exist #{config_path}"
|
878
|
+
end
|
879
|
+
|
880
|
+
|
881
|
+
begin
|
882
|
+
config_yml = YAML.load(ERB.new(File.read(config_path)).result)
|
883
|
+
rescue Exception => e
|
884
|
+
puts "Unable to parse #{config_path} as yml. Is this your Cucumber.yml file?"
|
885
|
+
raise ValidationError, e
|
886
|
+
end
|
887
|
+
|
888
|
+
if debug?
|
889
|
+
puts 'Parsed Cucumber config as:'
|
890
|
+
puts config_yml.inspect
|
891
|
+
end
|
892
|
+
|
893
|
+
profile = options[:profile]
|
894
|
+
unless profile
|
895
|
+
raise ValidationError, 'Config file provided but no profile selected.'
|
896
|
+
else
|
897
|
+
unless config_yml[profile]
|
898
|
+
raise ValidationError, "Config file provided did not contain profile #{profile}."
|
899
|
+
else
|
900
|
+
puts "Using profile #{profile}..."
|
901
|
+
self.profile = profile
|
902
|
+
end
|
903
|
+
end
|
904
|
+
else
|
905
|
+
if options[:profile]
|
906
|
+
raise ValidationError, 'Profile selected but no config file provided.'
|
907
|
+
end
|
908
|
+
end
|
909
|
+
|
910
|
+
self.config = config_path
|
911
|
+
end
|
912
|
+
|
913
|
+
end
|
914
|
+
|
915
|
+
|
916
|
+
end
|
917
|
+
end
|
918
|
+
|
919
|
+
|