xamarin-test-cloud 2.0.0.pre5 → 2.0.0

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