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.
@@ -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
@@ -0,0 +1,12 @@
1
+ ## Test Cloud Command Line
2
+
3
+ ### Testing
4
+
5
+ ```
6
+ $ bundle update
7
+ $ rake test # All tests.
8
+ $ rake unit # Unit tests.
9
+ $ rake integration # Integration tests.
10
+ $ rake spec # rspec tests
11
+ ```
12
+
@@ -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
+