xamarin-test-cloud-appium 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+