xamarin-test-cloud 2.0.0.pre5 → 2.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dead16bac1bbcbdb4ed7ff55bc656df5da6f5563
4
- data.tar.gz: 8298ea5c4fd1b0dc179a4285a78939ee154e8e0c
3
+ metadata.gz: d5e647b85d4a43752ebe4960f87b0860f5a3a73e
4
+ data.tar.gz: 499cba9a6428d4109b771d376d4fa028bd43367e
5
5
  SHA512:
6
- metadata.gz: 622d1602faa1a5f3e1e54b5dae8a6d8165542d659cffbda2d19f441277f1039a536a461449cd9005bd6e72bb52ab85f8616bc1dee4e89d16a8fed095221fa5ee
7
- data.tar.gz: 217a4ba762eebdbb7f69d815ce08df678e14646eab8e167a211dd69e2bf4532bd7fc4f73e370e679e34a951d5cda2cd2602ca823084a65967fa0d25379712b7f
6
+ metadata.gz: f1340fb676f6a14565b6dce3ebd3fa65f93ef165a29fa90d71a90fe2b749872489b22275dc40896ccb2728392cb951a4b3f2f938af86922ae5a101fb04ce9e77
7
+ data.tar.gz: a0adae20dd8f71db72eb0ed71d7791b79819cf63b89f66d03aa6e4923d7f698d30f0707ec92e50c6d426b8941c1757d57df4f4ab4409b16a61ca41957f917f02
data/CHANGELOG.md CHANGED
@@ -1,8 +1,14 @@
1
1
  ### 2.0.0
2
2
 
3
+ * C: appears in place of ./vendor directory when uploading tests from Windows
4
+ machines [#237](https://github.com/xamarinhq/test-cloud-frameworks/issues/237)
5
+ * Detecting the calabash android and calabash ios gem versions is broken
6
+ #51
7
+ * Don't send priority information to XTC #47
3
8
  * Set :send\_timeout on HTTPClient to allow uploads from flakey internet
4
9
  connections #45
5
10
  * Support Calabash 2.0 #43
11
+ * Needs to run in Jenkins CI (Windows/MacOS) and Travis Linux #42
6
12
  * Replace RestClient with HTTPClient
7
13
  * Set minimum ruby version to 2.0
8
14
  * Gem does not work in Windows environment: ffi cannot be found. #34
data/README.md CHANGED
@@ -11,8 +11,10 @@
11
11
  ```
12
12
  $ bundle update
13
13
  $ bundle exec rake test # All tests.
14
- $ bundle exec rake unit # Unit tests.
15
- $ bundle exec rake integration # Integration tests.
16
- $ bundle exec rake spec # rspec tests
14
+ $ bundle exec rake spec # rspec tests (unit and integration)
17
15
  ```
18
16
 
17
+ ### CI
18
+
19
+ * [Jenkins Windows 10](http://xtc-jenkins.xamdev.com/)
20
+ * [Travis Linux/macOS](https://travis-ci.com/xamarinhq/test-cloud-command-line/)
@@ -0,0 +1,175 @@
1
+
2
+ module XamarinTestCloud
3
+
4
+ # A class for determining the Calabash version.
5
+ #
6
+ # Finding the calabash version is required because:
7
+ #
8
+ # 1. The correct Android test server needs to be identified.
9
+ # 2. If a Gemfile does exist, a default one needs to be created.
10
+ #
11
+ # If a Gemfile and Gemfile.lock exists, use bundler to try to find the version.
12
+ #
13
+ # Otherwise, shell out to the gem's version command.
14
+ #
15
+ # Callers of `version` should rescue RuntimeError and re-raise.
16
+ #
17
+ # ArgumentError should not be caught. This is an internal class; ArgumentError
18
+ # represent incorrect usage.
19
+ class CalabashVersionDetector
20
+
21
+ def initialize(workspace, gem_keyword)
22
+ @workspace = workspace
23
+
24
+ if !File.directory?(workspace)
25
+ raise(ArgumentError, "Workspace must be a directory that exists")
26
+ end
27
+
28
+ case gem_keyword
29
+ when :android
30
+ @gem_name = "calabash-android"
31
+ @version_path = "calabash-android/version"
32
+ @version_constant = "Calabash::Android::VERSION"
33
+ when :ios
34
+ @gem_name = "calabash-cucumber"
35
+ @version_path = "calabash-cucumber/version"
36
+ @version_constant = "Calabash::Cucumber::VERSION"
37
+ when :calabash
38
+ @gem_name = "calabash"
39
+ @version_path = "calabash/version"
40
+ @version_constant = "Calabash::VERSION"
41
+ else
42
+ raise(ArgumentError,
43
+ %Q[Invalid gem_keyword: '#{gem_keyword}'; expected :android, :ios, or :calabash])
44
+ end
45
+ end
46
+
47
+ def to_s
48
+ %Q[#<CalabashVersion: #{workspace} #{gem_name} #{version_path} #{version_constant}>]
49
+ end
50
+
51
+ def inspect
52
+ to_s
53
+ end
54
+
55
+ def version
56
+ if use_bundler?
57
+ version = detect_version_with_bundler
58
+ else
59
+ version = detect_version_with_ruby
60
+ end
61
+ version
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :workspace, :gem_name, :version_path, :version_constant
67
+
68
+ def gemfile
69
+ File.join(workspace, "Gemfile")
70
+ end
71
+
72
+ def gemfile_lock
73
+ File.join(workspace, "Gemfile.lock")
74
+ end
75
+
76
+ def gemfile?
77
+ File.exist?(gemfile)
78
+ end
79
+
80
+ def gemfile_lock?
81
+ File.exist?(gemfile_lock)
82
+ end
83
+
84
+ def use_bundler?
85
+ gemfile? && gemfile_lock?
86
+ end
87
+
88
+ def detect_version_with_bundler
89
+ Dir.chdir(workspace) do
90
+ Bundler.with_clean_env do
91
+ bundle_install
92
+ command = %Q[bundle exec #{ruby_version_script}]
93
+ version = run_version_script(command)
94
+ if version != ""
95
+ version
96
+ else
97
+ nil
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def ruby_version_script
104
+ require_part = %Q[require '#{version_path}']
105
+ constant_part = %Q[puts #{version_constant}]
106
+ rescue_part = %Q[rescue LoadError => e]
107
+
108
+ command = %Q[begin;#{require_part};#{constant_part};#{rescue_part};puts '';end]
109
+
110
+ %Q[ruby -e "#{command}"]
111
+ end
112
+
113
+ def detect_version_with_ruby
114
+ Dir.chdir(workspace) do
115
+ Bundler.with_clean_env do
116
+ command = ruby_version_script
117
+ version = run_version_script(command)
118
+ if version != ""
119
+ version
120
+ else
121
+ nil
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ def run_version_script(command)
128
+ # The version script will always return a String
129
+ out = shell_out(command)
130
+
131
+ # Handle:
132
+ # Your version of bundler is outdated\n
133
+ # 0.19.1\n
134
+ lines = out.split($-0)
135
+
136
+ if lines.count == 0
137
+ nil
138
+ else
139
+ lines.last.strip
140
+ end
141
+ end
142
+
143
+ # Always called in the context of Dir.chdir, so there is no need to use
144
+ # --gemfile path/to/Gemfile argument.
145
+ def bundle_install
146
+ shell_out("bundle install")
147
+ end
148
+
149
+ # TODO Adopt command_runner?
150
+ # https://github.com/calabash/run_loop/blob/develop/lib/run_loop/shell.rb
151
+ # The problem with command_runner is that it has timeouts. We don't really
152
+ # want to manage time outs for potentially long-running tasks like
153
+ # $ bundle install
154
+ #
155
+ # TODO Handle encoding
156
+ # https://github.com/calabash/run_loop/blob/develop/lib/run_loop/encoding.rb
157
+ #
158
+ # Ruby reads stdout/stderr as US ASCII 8bit. I am not sure what the behavior
159
+ # "``" is.
160
+ def shell_out(command)
161
+ out = `#{command}`.strip
162
+ exit_code = $?.exitstatus
163
+ if exit_code != 0
164
+ raise RuntimeError, %Q[There was an error executing:
165
+
166
+ #{command}
167
+
168
+ exited #{exit_code}
169
+ ]
170
+ end
171
+
172
+ out
173
+ end
174
+ end
175
+ end
@@ -13,9 +13,13 @@ require 'xamarin-test-cloud/version'
13
13
  require 'xamarin-test-cloud/retriable_options'
14
14
  require 'xamarin-test-cloud/http/request'
15
15
  require 'xamarin-test-cloud/http/retriable_client'
16
+ require 'xamarin-test-cloud/environment'
17
+ require "xamarin-test-cloud/dsym"
18
+ require "xamarin-test-cloud/test_file"
19
+ require "xamarin-test-cloud/calabash_version_detector"
16
20
  require 'securerandom'
17
21
  require 'open3'
18
-
22
+ require "bundler"
19
23
 
20
24
  trap "SIGINT" do
21
25
  puts "Exiting"
@@ -31,10 +35,9 @@ module XamarinTestCloud
31
35
  include Thor::Actions
32
36
 
33
37
  attr_accessor :app, :api_key, :appname, :test_parameters, :user,
34
- :workspace, :config, :profile, :skip_config_check, :dry_run,
38
+ :config, :profile, :skip_config_check, :dry_run,
35
39
  :device_selection, :pretty, :async, :async_json, :priority, :endpoint_path,
36
- :locale, :series,
37
- :dsym, :session_id
40
+ :locale, :series, :session_id
38
41
 
39
42
  def self.exit_on_failure?
40
43
  true
@@ -123,7 +126,7 @@ module XamarinTestCloud
123
126
  :default => false
124
127
 
125
128
  method_option :priority,
126
- :desc => "Run as priority test execution. Please note: This is only available for some tiers, and priority test executions cost double.",
129
+ :desc => "REMOVED. You should not pass this option.",
127
130
  :type => :boolean,
128
131
  :default => false
129
132
 
@@ -181,6 +184,7 @@ module XamarinTestCloud
181
184
  puts "To test your app it needs to be compiled for release."
182
185
  puts "You can learn how to compile you app for release here:"
183
186
  puts "http://docs.xamarin.com/guides/android/deployment%2C_testing%2C_and_metrics/publishing_an_application/part_1_-_preparing_an_application_for_release"
187
+ # TODO Change this to: "Shared runtime apps are not supported."
184
188
  raise ValidationError, "Aborting"
185
189
  end
186
190
 
@@ -214,53 +218,32 @@ module XamarinTestCloud
214
218
 
215
219
  self.skip_config_check = options['skip-config-check']
216
220
 
217
- self.dsym= options['dsym-file']
218
- if dsym
219
- dsym_extension = File.extname(self.dsym)
220
- unless /dsym/i.match(dsym_extension) && File.directory?(dsym)
221
- raise ValidationError, "dsym-file must be a directory and have dSYM extension: #{dsym}"
222
- end
223
- end
224
-
225
- workspace_path = options[:workspace] || File.expand_path('.')
226
-
227
- unless File.directory?(workspace_path)
228
- raise ValidationError, "Provided workspace: #{workspace_path} is not a directory."
229
- end
230
-
231
- workspace_basename = File.basename(workspace_path)
232
- if workspace_basename.downcase == 'features'
233
- self.workspace = File.expand_path(File.join(workspace_path, '..'))
234
- puts "Deriving workspace #{self.workspace} from features folder #{workspace_basename}"
235
- else
236
- self.workspace = File.expand_path(workspace_path)
237
- end
238
-
239
- self.workspace = File.join(self.workspace, File::Separator)
240
-
241
-
242
- unless File.directory?(File.join(self.workspace, 'features'))
243
- log_header "Did not find features folder in workspace #{self.workspace}"
244
- puts "Either run the test-cloud command from the directory containing your features"
245
- puts "or use the --workspace option to refer to this directory"
246
- puts "See also test-cloud help submit"
247
- raise ValidationError, "Unable to find features folder in #{self.workspace}"
248
- end
221
+ # Resolves the workspace and sets the @derived_workspace variable.
222
+ expect_features_directory_in_workspace
249
223
 
224
+ # TODO: extract this method and the configuration branch below to a module
250
225
  parse_and_set_config_and_profile
251
226
  unless self.skip_config_check
252
- default_config = File.join(self.workspace, 'config', 'cucumber.yml')
227
+ default_config = File.join(derived_workspace, 'config', 'cucumber.yml')
228
+ # TODO .config/cucumber.yml is valid
229
+ # TODO cucumber.yaml is valid
253
230
  if File.exist?(default_config) && self.config.nil?
231
+ # TODO Reword the header.
254
232
  log_header 'Warning: Detected cucumber.yml config file, but no --config specified'
233
+
234
+ # TODO Reword the message and pass to raise as message.
255
235
  puts "Please specify --config #{default_config}"
256
236
  puts 'and specify a profile via --profile'
257
237
  puts 'If you know what you are doing you can skip this check with'
258
238
  puts '--skip-config-check'
239
+
240
+ # TODO This error message is wrong.
241
+ # It should be cucumber.yml detected by no --config option set.
259
242
  raise ValidationError, "#{default_config} detected but no profile selected."
260
243
  end
261
244
  end
262
245
 
263
- if debug?
246
+ if Environment.debug?
264
247
  puts "Host = #{self.host}"
265
248
  puts "User = #{self.user}"
266
249
  puts "App = #{self.app}"
@@ -268,7 +251,7 @@ module XamarinTestCloud
268
251
  puts "TestParams = #{self.test_parameters}"
269
252
  puts "API Key = #{self.api_key}"
270
253
  puts "Device Selection = #{self.device_selection}"
271
- puts "Workspace = #{self.workspace}"
254
+ puts "Workspace = #{derived_workspace}"
272
255
  puts "Config = #{self.config}"
273
256
  puts "Profile = #{self.profile}"
274
257
  puts "dSym = #{self.dsym}"
@@ -282,10 +265,12 @@ module XamarinTestCloud
282
265
  return
283
266
  end
284
267
  json = test_jon_data[:body]
285
- if debug?
268
+ if Environment.debug?
286
269
  p json
287
270
  end
288
271
 
272
+ warn_about_priority_flag(options)
273
+
289
274
  log_header('Test enqueued')
290
275
  puts "User: #{json['user_email']}"
291
276
  puts "Team: #{json['team']}" if json['team']
@@ -293,6 +278,7 @@ module XamarinTestCloud
293
278
 
294
279
  rejected_devices = json['rejected_devices']
295
280
  if rejected_devices && rejected_devices.size > 0
281
+ # TODO Break this into multiple lines and remove the parenthetical
296
282
  puts 'Skipping devices (you can update your selections via https://testcloud.xamarin.com)'
297
283
  rejected_devices.each { |d| puts d }
298
284
  end
@@ -317,7 +303,7 @@ module XamarinTestCloud
317
303
  @async_result[:error_messages] << e.message
318
304
  else
319
305
  raise
320
- end
306
+ end
321
307
 
322
308
  ensure
323
309
  $stdout = STDOUT
@@ -332,8 +318,43 @@ module XamarinTestCloud
332
318
 
333
319
  no_tasks do
334
320
 
335
- def debug?
336
- ENV['DEBUG'] == '1'
321
+ def dsym
322
+ if @dsym.nil?
323
+ value = options["dsym-file"]
324
+ if !value
325
+ @dsym = false
326
+ else
327
+ begin
328
+ @dsym = XamarinTestCloud::Dsym.new(value)
329
+ rescue RuntimeError => e
330
+ raise(ValidationError, e.message)
331
+ end
332
+ end
333
+ end
334
+ @dsym
335
+ end
336
+
337
+ # TODO Capture the log output when --async-json?; warn ==> stderr
338
+ def warn_about_priority_flag(options)
339
+ if options["priority"]
340
+
341
+ log_warn = lambda do |string|
342
+ if XamarinTestCloud::Environment.windows_env?
343
+ message = "WARN: #{string}"
344
+ else
345
+ message = "\033[34mWARN: #{string}\033[0m"
346
+ end
347
+ warn message
348
+ end
349
+
350
+ log_header("Detected Legacy Option")
351
+ puts ""
352
+ log_warn.call("The --priority option has been removed.")
353
+ log_warn.call("Priority execution is enabled automatically for Enterprise subscriptions.")
354
+ true
355
+ else
356
+ false
357
+ end
337
358
  end
338
359
 
339
360
  def process_async_log
@@ -353,13 +374,16 @@ module XamarinTestCloud
353
374
  JSON.parse(http_post("status_v3", {'id' => id, 'api_key' => api_key, 'user' => user}))
354
375
  end
355
376
 
356
- if debug?
377
+ if Environment.debug?
357
378
  log "Status JSON result:"
358
379
  puts status_json
359
380
  end
360
381
 
361
382
  wait_time = (Integer status_json["wait_time"] rescue nil) || 10
362
- wait_time = 1 if debug?
383
+ if Environment.debug?
384
+ wait_time = 1
385
+ end
386
+
363
387
  log status_json["message"]
364
388
 
365
389
  if status_json["exit_code"]
@@ -379,86 +403,84 @@ module XamarinTestCloud
379
403
  end
380
404
 
381
405
  def workspace_gemfile
382
- File.join(self.workspace, 'Gemfile')
406
+ File.join(derived_workspace, 'Gemfile')
383
407
  end
384
408
 
385
409
  def workspace_gemfile_lock
386
- File.join(self.workspace, 'Gemfile.lock')
410
+ File.join(derived_workspace, 'Gemfile.lock')
387
411
  end
388
412
 
389
413
  def submit_test_job(device_selection_data)
390
414
  tmpdir = Dir.mktmpdir
391
- if debug?
415
+ if Environment.debug?
392
416
  log "Packaging gems in: #{tmpdir}"
393
417
  end
394
418
 
395
- server = verify_app_and_extract_test_server
396
-
397
- log_header('Checking for Gemfile')
419
+ # TODO Move to common expect_valid_application
420
+ # TODO Rename: does not need to extract a test server
421
+ verify_app_and_extract_test_server
398
422
 
399
- if File.exist?(workspace_gemfile)
400
- FileUtils.cp workspace_gemfile, tmpdir
401
- FileUtils.cp workspace_gemfile_lock, tmpdir if File.exist?(workspace_gemfile_lock)
402
- else
403
- copy_default_gemfile(File.join(tmpdir, "Gemfile"), server)
404
- end
423
+ stage_gemfile_and_bundle_package(tmpdir)
405
424
 
406
- log_header('Packaging')
425
+ log_header('Verifying dependencies')
407
426
 
408
- ENV['BUNDLE_GEMFILE'] = File.join(tmpdir, "Gemfile")
409
- FileUtils.cd(self.workspace) do
410
- if self.async_json
411
- bundle_log, status = Open3.capture2e('bundle package --all')
412
- puts bundle_log
413
- else
414
- system('bundle package --all')
415
- status = $?
416
- end
417
- if status != 0
418
- log_and_abort 'Bundler failed. Please check command: bundle package'
419
- end
427
+ # TODO Move to common expect_valid_application
428
+ if is_android?
429
+ expect_android_test_server
420
430
  end
421
- log_header('Verifying dependencies')
422
- verify_dependencies(tmpdir)
423
431
 
432
+ # TODO this happens too soon, it should collect the files to upload
424
433
  if dry_run
425
434
  log_header('Dry run only')
426
435
  log('Dry run completed OK')
427
436
  return
428
437
  end
429
438
 
430
- app_file, dsym_zip, files, paths = gather_files_and_paths_to_upload(all_files(tmpdir), tmpdir)
439
+ # TestFile instances
440
+ test_suite_files = collect_test_suite_files(tmpdir)
441
+
442
+ negotiated = negotiate_contents_of_upload(test_suite_files)
443
+ app_digest_or_file = negotiated[:app_digest_or_file]
444
+ dsym_digest_or_file = negotiated[:dsym_digest_or_file]
445
+ digests_and_files = negotiated[:digests_and_files]
446
+ remote_paths = negotiated[:remote_paths]
431
447
 
432
448
  log_header('Uploading negotiated files')
433
449
 
434
- upload_data = {'files' => files,
435
- 'paths' => paths,
436
- 'user' => self.user,
437
- 'client_version' => XamarinTestCloud::VERSION,
438
- 'app_file' => app_file,
439
- 'device_selection' => device_selection_data,
440
- 'app' => self.appname,
441
- 'test_parameters' => self.test_parameters,
442
- 'locale' => self.locale,
443
- 'series' => self.series,
444
- 'api_key' => api_key,
445
- 'dsym_file' => dsym_zip,
446
- 'dsym_filename' => dsym_file_name,
447
- 'app_filename' => File.basename(app),
448
- 'priority' => self.priority}
450
+ upload_data = {
451
+ 'files' => digests_and_files,
452
+ 'paths' => remote_paths,
453
+ 'user' => self.user,
454
+ 'client_version' => XamarinTestCloud::VERSION,
455
+ 'app_file' => app_digest_or_file,
456
+ 'device_selection' => device_selection_data,
457
+ 'app' => self.appname,
458
+ 'test_parameters' => self.test_parameters,
459
+ 'locale' => self.locale,
460
+ 'series' => self.series,
461
+ 'api_key' => api_key,
462
+ 'app_filename' => File.basename(app)
463
+ }
464
+
465
+ if dsym
466
+ upload_data["dsym_file"] = dsym_digest_or_file
467
+ upload_data["dsym_filename"] = dsym.remote_path(app)
468
+ else
469
+ upload_data["dsym_file"] = nil
470
+ upload_data["dsym_filename"] = nil
471
+ end
449
472
 
450
473
  if profile #only if config and profile
451
474
  upload_data['profile'] = profile
452
475
  end
453
476
 
454
- if debug?
477
+ if Environment.debug?
455
478
  puts JSON.pretty_generate(upload_data)
456
479
  end
457
480
 
481
+ contains_file = digests_and_files.find { |f| f.is_a?(File) }
458
482
 
459
- contains_file = files.find { |f| f.is_a?(File) }
460
-
461
- contains_file = contains_file || app_file.is_a?(File)
483
+ contains_file = contains_file || app_digest_or_file.is_a?(File)
462
484
 
463
485
  if contains_file
464
486
  self.endpoint_path = FILE_UPLOAD_ENDPOINT #nginx receives upload
@@ -466,12 +488,12 @@ module XamarinTestCloud
466
488
  self.endpoint_path = FORM_URL_ENCODED_ENDPOINT #ruby receives upload
467
489
  end
468
490
 
469
- if debug?
491
+ if Environment.debug?
470
492
  puts "Will upload to file path: #{self.endpoint_path}"
471
493
  end
472
494
 
473
495
  response = http_post(endpoint_path, upload_data) do |response, request|
474
- if debug?
496
+ if Environment.debug?
475
497
  puts "Request url: #{self.host}/#{request.route}"
476
498
  puts "Response code: #{response.code}"
477
499
  puts "Response body: #{response.body}"
@@ -498,103 +520,110 @@ module XamarinTestCloud
498
520
 
499
521
  end
500
522
 
501
-
502
- def copy_default_gemfile(gemfile_path, server)
503
- log('')
504
- log('Gemfile missing.')
505
- log('You must provide a Gemfile in your workspace.')
506
- log('A Gemfile must describe your dependencies and their versions')
507
- log('See: http://gembundler.com/v1.3/gemfile.html')
508
- log('')
509
- log('Warning proceeding with default Gemfile.')
510
- log('It is strongly recommended that you create a custom Gemfile.')
511
-
512
- File.open(gemfile_path, "w") do |f|
513
- f.puts "source 'http://rubygems.org'"
514
- if is_android?
515
- f.puts "gem 'calabash-android', '#{calabash_android_version}'"
516
- elsif is_ios?
517
- f.puts "gem 'calabash-cucumber', '#{calabash_ios_version}'"
518
- else
519
- raise ValidationError, 'Your app must be an ipa or apk file.'
520
- end
523
+ def app_and_dsym_details
524
+ dsym_digest = nil
525
+ dsym_file = nil
526
+ if dsym
527
+ dsym_file = dsym.symbol_file
528
+ dsym_digest = digest(dsym_file)
521
529
  end
522
- log("Proceeding with Gemfile: #{gemfile_path}")
523
530
 
524
- puts(File.read(gemfile_path))
531
+ {
532
+ :app_digest => digest(app),
533
+ :dsym_digest => dsym_digest,
534
+ :dsym_file => dsym_file
535
+ }
525
536
 
526
- log('')
527
537
  end
528
538
 
529
- def gather_files_and_paths_to_upload(collected_files, tmpdir)
530
-
531
- log_header('Calculating digests')
539
+ def http_fetch_remote_digests(file_digests, app_digest, dsym_digest)
540
+ parameters = {
541
+ "hashes" => file_digests,
542
+ "app_hash" => app_digest,
543
+ "dsym_hash" => dsym_digest
544
+ }
532
545
 
533
- file_paths = collected_files[:files]
534
- feature_prefix = collected_files[:feature_prefix]
535
- workspace_prefix = collected_files[:workspace_prefix]
546
+ response = http_post("check_hash", parameters)
536
547
 
537
- hashes = file_paths.collect { |f| digest(f) }
548
+ JSON.parse(response)
549
+ end
538
550
 
539
- if hashes.nil? || hashes.size == 0
540
- hashes << '0222222222222222222222222222222222222222222222222222222222222222'
551
+ # @param [Array<TestFiles>] test_files A list of TestFile instances.
552
+ # @param [Hash] cache_status <digest> => true/false pairs based on whether
553
+ # or not the server (Test Cloud) already has this file.
554
+ def negotiated_digests_files_and_remote_paths(test_files, cache_status)
555
+ digests_and_files = []
556
+ remote_paths = []
557
+ test_files.each do |test_file|
558
+ if cache_status[test_file.digest]
559
+ # Server already knows about this file; don't upload it.
560
+ digests_and_files << test_file.digest
561
+ else
562
+ # Upload file
563
+ digests_and_files << test_file.file_instance
564
+ end
565
+ remote_paths << test_file.remote_path
541
566
  end
542
567
 
568
+ {
569
+ :digests_and_files => digests_and_files,
570
+ :remote_paths => remote_paths
571
+ }
572
+ end
543
573
 
544
- log_header('Negotiating upload')
574
+ # test_files is a list of TestFile instance for the assets in:
575
+ #
576
+ # 1. <tmpdir>/vendor/cache/*
577
+ # 2. features/**/*
578
+ # 3. test_server/<the matching test server>
579
+ # 4. The cucumber configuration file
580
+ #
581
+ # 3 and 4 are only present if they exist.
582
+ #
583
+ # @param [Array<TestFile>] test_files a list of TestFile instances
584
+ def negotiate_contents_of_upload(test_files)
585
+ log_header('Calculating digests')
545
586
 
546
- app_digest = digest(app)
587
+ file_digests = collect_test_suite_file_digests(test_files)
547
588
 
548
- dsym_digest= nil
549
- if dsym
550
- FileUtils.cp_r(dsym, tmpdir)
551
- files_in_dwarf = Dir.glob(File.join(tmpdir, File.basename(dsym), 'Contents', 'Resources', 'DWARF', '*'))
552
- unless files_in_dwarf.count == 1
553
- raise ValidationError, "dSym #{dsym} contains more than one file in Contents/Resources/DWARF: #{files_in_dwarf}"
554
- end
555
-
556
- dsym_abs_path= files_in_dwarf.first
557
- dsym_digest = digest(dsym_abs_path)
558
- end
559
- out = {'hashes' => hashes, 'app_hash' => app_digest, 'dsym_hash' => dsym_digest}
589
+ log_header('Negotiating upload')
560
590
 
561
- response = http_post('check_hash', out)
591
+ app_and_dsym = app_and_dsym_details
592
+ app_digest = app_and_dsym[:app_digest]
593
+ dsym_digest = app_and_dsym[:dsym_digest]
562
594
 
563
- cache_status = JSON.parse(response)
595
+ server_digests = http_fetch_remote_digests(file_digests, app_digest, dsym_digest)
564
596
 
565
597
  log_header('Gathering files based on negotiation')
566
598
 
567
- files = []
568
- paths = []
569
- file_paths.each do |file|
570
- if cache_status[digest(file)]
571
- #Server already knows about this file. No need to upload it.
572
- files << digest(file)
573
- else
574
- #Upload file
575
- files << File.new(file)
576
- end
599
+ negotiated = negotiated_digests_files_and_remote_paths(test_files,
600
+ server_digests)
577
601
 
578
- if file.start_with?(feature_prefix)
579
- prefix = feature_prefix
580
- else
581
- prefix = workspace_prefix
582
- end
583
- paths << file.sub(prefix, '').sub("#{tmpdir}/", '')
584
- end
602
+ digests_and_files = negotiated[:digests_and_files]
603
+ remote_paths = negotiated[:remote_paths]
585
604
 
586
- if config
587
- files << File.new(config)
588
- paths << 'config/cucumber.yml'
605
+ if server_digests[app_digest]
606
+ app_digest_or_file = app_digest
607
+ else
608
+ app_digest_or_file = File.new(app)
589
609
  end
590
610
 
591
- app_file = cache_status[app_digest] ? app_digest : File.new(app)
592
-
593
- if dsym_digest
594
- dsym_file = cache_status[dsym_digest] ? dsym_digest : File.new(dsym_abs_path)
611
+ dsym_digest_or_file = nil
612
+ if dsym
613
+ if server_digests[dsym_digest]
614
+ dsym_digest_or_file = dsym_digest
615
+ else
616
+ dsym_file = app_and_dsym[:dsym_file]
617
+ dsym_digest_or_file = File.new(dsym_file)
618
+ end
595
619
  end
596
620
 
597
- return app_file, dsym_file, files, paths
621
+ {
622
+ :app_digest_or_file => app_digest_or_file,
623
+ :dsym_digest_or_file => dsym_digest_or_file,
624
+ :digests_and_files => digests_and_files,
625
+ :remote_paths => remote_paths
626
+ }
598
627
  end
599
628
 
600
629
  def digest(file)
@@ -620,132 +649,166 @@ module XamarinTestCloud
620
649
  end
621
650
 
622
651
  def is_calabash_2?
623
- detect_calabash_2
652
+ calabash_2_version
624
653
  end
625
654
 
626
- def get_calabash_version_from_bundler
627
- cmd = "bundle exec ruby -e \"begin; require 'calabash/version';puts Calabash::VERSION;rescue LoadError => e; puts '';end\""
628
-
629
- `#{cmd}`.strip
655
+ # Valid gem keywords are: :calabash, :android, :ios
656
+ # This is potentially very expensive because there is at least one shell
657
+ # call and possibly a `bundle install`. We only want to call this method
658
+ # once.
659
+ def detect_calabash_version(gem_keyword)
660
+ begin
661
+ # Raises RuntimeError if there is an error shelling out.
662
+ # Raises ArgumentError on incorrect (logical) usage.
663
+ # Returns nil if there is no version information available.
664
+ CalabashVersionDetector.new(derived_workspace, gem_keyword).version
665
+ rescue RuntimeError => e
666
+ raise(ValidationError, e.message)
667
+ end
630
668
  end
631
669
 
632
- def detect_calabash_2
633
- @detected_calabash_2 ||= lambda do
634
- log "Auto-detecting if Calabash 2.0 testsuite" if debug?
635
-
636
- # Implicitly detect
637
- if File.exist?(workspace_gemfile)
638
- FileUtils.cd(self.workspace) do
639
- version = get_calabash_version_from_bundler
640
-
641
- if version.split('.').first.to_i >= 2
642
- log "It is a Calabash 2.0 testsuite" if debug?
643
- return true
644
- end
645
- end
670
+ # Memoize: we only want to call detect_calabash_version once.
671
+ def calabash_2_version
672
+ if @calabash_2_version.nil?
673
+ version = detect_calabash_version(:calabash)
674
+ if version
675
+ @calabash_2_version = version
676
+ else
677
+ @calabash_2_version = false
646
678
  end
647
-
648
- false
649
- end.call
679
+ end
680
+ @calabash_2_version
650
681
  end
651
682
 
652
- def calabash_2_version
653
- version = nil
683
+ # TODO: this should be called as part of a validation step.
684
+ # Memoize: we only want to call detect_calabash_version once.
685
+ def calabash_android_version
686
+ return calabash_2_version if is_calabash_2?
654
687
 
655
- if File.exist?(workspace_gemfile)
656
- FileUtils.cd(self.workspace) do
657
- version = get_calabash_version_from_bundler
688
+ if @calabash_android_version.nil?
689
+ version = detect_calabash_version(:android)
690
+ if version
691
+ @calabash_android_version = version
692
+ else
693
+ @calabash_android_version = false
658
694
  end
659
695
  end
696
+ @calabash_android_version
697
+ end
660
698
 
661
- unless version
662
- require 'calabash'
663
- version = Calabash::VERSION
699
+ # TODO: this should be called as part of a validation step.
700
+ # Memoize: we only want to call detect_calabash_version once.
701
+ def calabash_ios_version
702
+ return calabash_2_version if is_calabash_2?
703
+
704
+ if @calabash_ios_version.nil?
705
+ version = detect_calabash_version(:ios)
706
+ if version
707
+ @calabash_ios_version = version
708
+ else
709
+ @calabash_ios_version = false
710
+ end
664
711
  end
665
712
 
666
- version.strip
713
+ @calabash_ios_version
667
714
  end
668
715
 
669
- def calabash_android_version
670
- if is_calabash_2?
671
- return calabash_2_version
672
- end
716
+ # TODO What to do about logical errors?
717
+ def android_test_server
718
+ @android_test_server ||= begin
719
+ if !is_android?
720
+ raise RuntimeError, %Q[
721
+ This method cannot be called on a non-android project.
673
722
 
674
- version = nil
723
+ Please send a bug report to testcloud@xamarin.com that includes:
675
724
 
676
- if File.exist?(workspace_gemfile)
677
- FileUtils.cd(self.workspace) do
678
- version = `bundle exec ruby -e "require 'calabash-android/version'; puts Calabash::Android::VERSION"`
679
- version = version && version.strip
725
+ 1. The exact command you are running.
726
+ 2. The following stack trace.
727
+ ]
728
+ else
729
+ digest = XamarinTestCloud::TestFile.file_digest(app, :MD5)
730
+ name = "#{digest}_#{calabash_android_version}.apk"
731
+ basedir = derived_workspace
732
+ file = File.join(basedir, 'test_servers', name)
733
+ TestFile.new(file, basedir)
680
734
  end
681
735
  end
682
-
683
- unless version
684
- require 'calabash-android'
685
- version = Calabash::Android::VERSION
686
- end
687
-
688
- version.strip
689
736
  end
690
737
 
691
- def calabash_ios_version
692
- if is_calabash_2?
693
- return calabash_2_version
694
- end
738
+ def expect_android_test_server
739
+ return nil if !is_android?
695
740
 
696
- version = nil
741
+ path = android_test_server.path
742
+ return true if File.exist?(path)
697
743
 
698
- if File.exist?(workspace_gemfile)
699
- FileUtils.cd(self.workspace) do
700
- version = `bundle exec ruby -e "require 'calabash-cucumber/version'; puts Calabash::Cucumber::VERSION"`
701
- version = version && version.strip
702
- end
744
+ if is_calabash_2?
745
+ build_command = "calabash build #{app}"
746
+ else
747
+ build_command = "calabash-android build #{app}"
703
748
  end
704
749
 
705
- unless version
706
- require 'calabash-cucumber'
707
- version = Calabash::Cucumber::VERSION
708
- end
750
+ raise ValidationError, %Q[
751
+ No test server '#{path}' found. Please run:
709
752
 
710
- version.strip
711
- end
753
+ #{build_command}
712
754
 
713
- def test_server_path
714
- require 'digest/md5'
715
- digest = Digest::MD5.file(app).hexdigest
716
- File.join(self.workspace, 'test_servers', "#{digest}_#{calabash_android_version}.apk")
755
+ and try submitting again.
756
+ ]
717
757
  end
718
758
 
719
- def all_files(tmpdir)
720
- dir = workspace
721
-
722
- files = Dir.glob(File.join("#{dir}features", '**', '*'))
723
-
724
- if File.directory?("#{dir}playback")
725
- files += Dir.glob(File.join("#{dir}playback", '*'))
726
- end
759
+ def collect_test_suite_files(tmpdir)
760
+ files = collect_files_from_features +
761
+ collect_files_from_tmpdir(tmpdir) +
762
+ collect_gemfile_files(tmpdir)
727
763
 
728
764
  if config
729
765
  files << config
730
766
  end
731
767
 
732
- files += Dir.glob(File.join(tmpdir, "vendor", 'cache', '*'))
768
+ if is_android?
769
+ files << android_test_server
770
+ end
771
+
772
+ files
773
+ end
733
774
 
734
- if workspace and workspace.strip != ''
735
- files += Dir.glob("#{workspace}Gemfile")
736
- files += Dir.glob("#{workspace}Gemfile.lock")
775
+ # Returns file paths as Strings
776
+ def collect_files_with_glob(glob)
777
+ Dir.glob(glob).select do |path|
778
+ File.file?(path)
737
779
  end
780
+ end
738
781
 
739
- if is_android?
740
- files << test_server_path
782
+ # Returns TestFile instances.
783
+ def collect_files_from_features
784
+ basedir = derived_workspace
785
+ glob = File.join(basedir, "features", "**", "*")
786
+ collect_files_with_glob(glob).map do |file|
787
+ TestFile.new(file, basedir)
741
788
  end
789
+ end
742
790
 
743
- {:feature_prefix => dir, :workspace_prefix => workspace, :files => files.find_all { |file_or_dir| File.file? file_or_dir }}
791
+ # Returns TestFile instances.
792
+ def collect_files_from_tmpdir(tmpdir)
793
+ glob = File.join(tmpdir, "vendor", "cache", "*")
794
+ collect_files_with_glob(glob).map do |file|
795
+ TestFile.new(file, tmpdir)
796
+ end
744
797
  end
745
798
 
746
- def dsym_file_name
747
- if dsym
748
- "#{File.basename(self.app)}_dSym"
799
+ # Returns TestFile instances.
800
+ def collect_gemfile_files(tmpdir)
801
+ glob = File.join(tmpdir, "{Gemfile,Gemfile.lock}")
802
+ collect_files_with_glob(glob).map do |file|
803
+ TestFile.new(file, tmpdir)
804
+ end
805
+ end
806
+
807
+ # Returns a list of digests
808
+ # @param [Array<TestFile>] test_files A list of TestFile instances.
809
+ def collect_test_suite_file_digests(test_files)
810
+ test_files.map do |test_file|
811
+ test_file.digest
749
812
  end
750
813
  end
751
814
 
@@ -797,14 +860,10 @@ module XamarinTestCloud
797
860
  end
798
861
  end
799
862
 
800
- def is_windows?
801
- (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
802
- end
803
-
804
- def is_macosx?
805
- (RbConfig::CONFIG['host_os'] =~ /darwin/)
806
- end
807
-
863
+ # TODO: move to separate module/class
864
+ # TODO: fix the detection algorithm - it is broken
865
+ # TODO: remove the Frank check
866
+ # https://github.com/xamarinhq/test-cloud-command-line/issues/19
808
867
  def validate_ipa(ipa)
809
868
  result = false
810
869
  dir = Dir.mktmpdir #do |dir|
@@ -846,14 +905,14 @@ module XamarinTestCloud
846
905
  end
847
906
 
848
907
  def log_header(message)
849
- if is_windows?
908
+ if XamarinTestCloud::Environment.windows_env?
850
909
  puts "\n### #{message} ###"
851
910
  else
852
911
  puts "\n\e[#{35}m### #{message} ###\e[0m"
853
912
  end
854
913
  end
855
914
 
856
-
915
+ # TODO: rename: does not need to extract or return the server
857
916
  def verify_app_and_extract_test_server
858
917
  server = nil
859
918
 
@@ -863,7 +922,7 @@ module XamarinTestCloud
863
922
  unless (/\.(apk|ipa)$/ =~ app)
864
923
  raise ValidationError, '<APP> should be either an ipa or apk file.'
865
924
  end
866
- if is_ios? and is_macosx?
925
+ if is_ios? && Environment.macos_env?
867
926
  log_header('Checking ipa for linking with Calabash')
868
927
  server = validate_ipa(app)
869
928
  abort_unless(server) do
@@ -874,11 +933,15 @@ module XamarinTestCloud
874
933
  server
875
934
  end
876
935
 
936
+ # TODO Rename or remove; abort is not the right verb - use exit
937
+ # +1 for remove
877
938
  def abort(&block)
878
939
  yield block
879
940
  exit 1
880
941
  end
881
942
 
943
+ # TODO Rename or remove; abort is not the right verb - use exit
944
+ # +1 for remove
882
945
  def abort_unless(condition, &block)
883
946
  unless condition
884
947
  yield block
@@ -886,6 +949,8 @@ module XamarinTestCloud
886
949
  end
887
950
  end
888
951
 
952
+ # TODO Rename or remove; abort is not the right verb - use exit
953
+ # +1 for remove
889
954
  def log_and_abort(message)
890
955
  raise XamarinTestCloud::ValidationError.new(message) if self.async_json
891
956
  abort do
@@ -894,7 +959,7 @@ module XamarinTestCloud
894
959
  end
895
960
  end
896
961
 
897
-
962
+ # TODO Move to a module
898
963
  def shared_runtime?(app_path)
899
964
  f = files(app_path)
900
965
  f.any? do |file|
@@ -905,6 +970,7 @@ module XamarinTestCloud
905
970
  end
906
971
  end
907
972
 
973
+ # TODO One caller #shared_runtime? move to a module
908
974
  def files(app)
909
975
  Zip::File.open(app) do |zip_file|
910
976
  zip_file.collect do |entry|
@@ -913,44 +979,22 @@ module XamarinTestCloud
913
979
  end
914
980
  end
915
981
 
916
- def verify_dependencies(path)
917
- if is_android?
918
- abort_unless(File.exist?(test_server_path)) do
919
- puts "No test server '#{test_server_path}' found. Please run:"
920
-
921
- if is_calabash_2?
922
- puts " calabash build #{app}"
923
- else
924
- puts " calabash-android build #{app}"
925
- end
926
- end
982
+ # TODO: extract to a module
983
+ # TODO: replace unless/else branches
984
+ def parse_and_set_config_and_profile
985
+ config_path = options[:config]
927
986
 
928
- if is_calabash_2?
929
- calabash_gem = Dir.glob("#{path}/vendor/cache/calabash-*").first
930
- abort_unless(calabash_gem) do
931
- puts 'calabash was not packaged correct.'
932
- puts 'Please tell testcloud@xamarin.com about this bug.'
933
- end
934
- else
935
- calabash_gem = Dir.glob("#{path}/vendor/cache/calabash-android-*").first
936
- abort_unless(calabash_gem) do
937
- puts 'calabash-android was not packaged correct.'
938
- puts 'Please tell testcloud@xamarin.com about this bug.'
939
- end
940
- end
987
+ if !config_path
988
+ self.config = false
989
+ return
941
990
  end
942
- end
943
-
944
991
 
945
- def parse_and_set_config_and_profile
946
- config_path = options[:config]
947
992
  if config_path
948
993
  config_path = File.expand_path(config_path)
949
994
  unless File.exist?(config_path)
950
995
  raise ValidationError, "Config file does not exist #{config_path}"
951
996
  end
952
997
 
953
-
954
998
  begin
955
999
  config_yml = YAML.load(ERB.new(File.read(config_path)).result)
956
1000
  rescue Exception => e
@@ -958,7 +1002,7 @@ module XamarinTestCloud
958
1002
  raise ValidationError, e
959
1003
  end
960
1004
 
961
- if debug?
1005
+ if Environment.debug?
962
1006
  puts 'Parsed Cucumber config as:'
963
1007
  puts config_yml.inspect
964
1008
  end
@@ -980,13 +1024,147 @@ module XamarinTestCloud
980
1024
  end
981
1025
  end
982
1026
 
983
- self.config = config_path
1027
+ self.config = TestFile.cucumber_config(config_path)
984
1028
  end
985
1029
 
986
- end
1030
+ def expect_features_directory_in_workspace
1031
+ path = derived_workspace
1032
+ features = File.join(path, "features")
987
1033
 
1034
+ return true if File.directory?(features)
1035
+ log_header("Missing features Directory")
1036
+ raise(ValidationError, %Q[Did not find a features directory in workspace:
988
1037
 
989
- end
990
- end
1038
+ #{path}
1039
+
1040
+ You have two options:
1041
+
1042
+ 1. Run the test-cloud submit command in the directory that contains your
1043
+ features directory or
1044
+ 2. Use the --workspace option point to the directory that contains your
1045
+ features directory.
1046
+
1047
+ See also
1048
+
1049
+ $ test-cloud help submit
1050
+ ])
1051
+ end
1052
+
1053
+ def derived_workspace
1054
+ @derived_workspace ||= begin
1055
+ path = detect_workspace(options)
1056
+ expect_workspace_directory(path)
1057
+
1058
+ workspace_basename = File.basename(path)
1059
+ if workspace_basename.downcase == "features"
1060
+ derived = File.expand_path(File.join(path, ".."))
1061
+ log("Deriving workspace as: #{derived} from features folder")
1062
+ derived
1063
+ else
1064
+ File.expand_path(path)
1065
+ end
1066
+ end
1067
+ end
1068
+
1069
+ def detect_workspace(options)
1070
+ options["workspace"] || File.expand_path(".")
1071
+ end
1072
+
1073
+ def expect_workspace_directory(path)
1074
+ message = nil
1075
+ if !File.exist?(path)
1076
+ message = %Q[The path specified by --workspace:
1077
+
1078
+ #{path}
1079
+
1080
+ does not exist.
1081
+ ]
1082
+ elsif !File.directory?(path)
1083
+ message = %Q[The path specified by --workspace:
1084
+
1085
+ #{path}
1086
+
1087
+ is not a directory.
1088
+ ]
1089
+ end
1090
+
1091
+ if message
1092
+ raise(ValidationError, message)
1093
+ else
1094
+ true
1095
+ end
1096
+ end
1097
+
1098
+ # TODO We should require a Gemfile
1099
+ # TODO Untested
1100
+ def copy_default_gemfile(gemfile_path)
1101
+ log('')
1102
+ log('Gemfile missing.')
1103
+ log('You must provide a Gemfile in your workspace.')
1104
+ log('A Gemfile must describe your dependencies and their versions')
1105
+ log('See: http://bundler.io/')
1106
+ log('')
1107
+ log('Warning proceeding with default Gemfile.')
1108
+ log('It is strongly recommended that you create a custom Gemfile.')
1109
+
1110
+ File.open(gemfile_path, "w") do |f|
1111
+ f.puts "source 'http://rubygems.org'"
1112
+ if is_android?
1113
+ f.puts "gem 'calabash-android', '#{calabash_android_version}'"
1114
+ elsif is_ios?
1115
+ f.puts "gem 'calabash-cucumber', '#{calabash_ios_version}'"
1116
+ else
1117
+ raise ValidationError, 'Your app must be an ipa or apk file.'
1118
+ end
1119
+ end
1120
+ log("Proceeding with Gemfile: #{gemfile_path}")
1121
+
1122
+ puts(File.read(gemfile_path))
1123
+
1124
+ log('')
1125
+ end
991
1126
 
1127
+ def stage_gemfile_and_bundle_package(tmpdir)
1128
+ log_header('Checking for Gemfile')
992
1129
 
1130
+ if File.exist?(workspace_gemfile)
1131
+ FileUtils.cp(workspace_gemfile, tmpdir)
1132
+ if File.exist?(workspace_gemfile_lock)
1133
+ FileUtils.cp(workspace_gemfile_lock, tmpdir)
1134
+ end
1135
+ else
1136
+ copy_default_gemfile(File.join(tmpdir, "Gemfile"))
1137
+ end
1138
+ gemfile = File.join(tmpdir, "Gemfile")
1139
+ bundle_package(gemfile)
1140
+ end
1141
+
1142
+ def bundle_package(gemfile)
1143
+ log_header('Packaging')
1144
+
1145
+ Bundler.with_clean_env do
1146
+ args = ["package", "--all", "--gemfile", gemfile]
1147
+ success = shell_out_with_system("bundle", args)
1148
+ if !success
1149
+ cmd = "bundle #{args.join(" ")}"
1150
+ raise(ValidationError, %Q[Could not package gems. This command failed:
1151
+
1152
+ #{cmd}
1153
+
1154
+ Check your local Gemfile and and the remote Gemfile at:
1155
+
1156
+ #{gemfile})
1157
+ ])
1158
+ end
1159
+ end
1160
+ true
1161
+ end
1162
+
1163
+ # stderr will not be captured during --async-json
1164
+ def shell_out_with_system(command, arguments)
1165
+ system(command, *arguments)
1166
+ $?.exitstatus == 0
1167
+ end
1168
+ end
1169
+ end
1170
+ end