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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +5 -3
- data/lib/xamarin-test-cloud/calabash_version_detector.rb +175 -0
- data/lib/xamarin-test-cloud/cli.rb +477 -299
- data/lib/xamarin-test-cloud/dsym.rb +105 -0
- data/lib/xamarin-test-cloud/environment.rb +80 -0
- data/lib/xamarin-test-cloud/test_file.rb +94 -0
- data/lib/xamarin-test-cloud/version.rb +1 -1
- metadata +36 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5e647b85d4a43752ebe4960f87b0860f5a3a73e
|
4
|
+
data.tar.gz: 499cba9a6428d4109b771d376d4fa028bd43367e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
:
|
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 => "
|
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
|
-
|
218
|
-
|
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(
|
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 = #{
|
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
|
-
|
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
|
336
|
-
|
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
|
-
|
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(
|
406
|
+
File.join(derived_workspace, 'Gemfile')
|
383
407
|
end
|
384
408
|
|
385
409
|
def workspace_gemfile_lock
|
386
|
-
File.join(
|
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
|
-
|
396
|
-
|
397
|
-
|
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
|
-
|
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('
|
425
|
+
log_header('Verifying dependencies')
|
407
426
|
|
408
|
-
|
409
|
-
|
410
|
-
|
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
|
-
|
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 = {
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
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 =
|
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
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
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
|
-
|
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
|
530
|
-
|
531
|
-
|
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
|
-
|
534
|
-
feature_prefix = collected_files[:feature_prefix]
|
535
|
-
workspace_prefix = collected_files[:workspace_prefix]
|
546
|
+
response = http_post("check_hash", parameters)
|
536
547
|
|
537
|
-
|
548
|
+
JSON.parse(response)
|
549
|
+
end
|
538
550
|
|
539
|
-
|
540
|
-
|
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
|
-
|
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
|
-
|
587
|
+
file_digests = collect_test_suite_file_digests(test_files)
|
547
588
|
|
548
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
568
|
-
|
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
|
-
|
579
|
-
|
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
|
587
|
-
|
588
|
-
|
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
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
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
|
-
|
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
|
-
|
652
|
+
calabash_2_version
|
624
653
|
end
|
625
654
|
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
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
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
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
|
-
|
649
|
-
end.call
|
679
|
+
end
|
680
|
+
@calabash_2_version
|
650
681
|
end
|
651
682
|
|
652
|
-
|
653
|
-
|
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
|
656
|
-
|
657
|
-
|
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
|
-
|
662
|
-
|
663
|
-
|
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
|
-
|
713
|
+
@calabash_ios_version
|
667
714
|
end
|
668
715
|
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
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
|
-
|
723
|
+
Please send a bug report to testcloud@xamarin.com that includes:
|
675
724
|
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
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
|
692
|
-
if
|
693
|
-
return calabash_2_version
|
694
|
-
end
|
738
|
+
def expect_android_test_server
|
739
|
+
return nil if !is_android?
|
695
740
|
|
696
|
-
|
741
|
+
path = android_test_server.path
|
742
|
+
return true if File.exist?(path)
|
697
743
|
|
698
|
-
if
|
699
|
-
|
700
|
-
|
701
|
-
|
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
|
-
|
706
|
-
|
707
|
-
version = Calabash::Cucumber::VERSION
|
708
|
-
end
|
750
|
+
raise ValidationError, %Q[
|
751
|
+
No test server '#{path}' found. Please run:
|
709
752
|
|
710
|
-
|
711
|
-
end
|
753
|
+
#{build_command}
|
712
754
|
|
713
|
-
|
714
|
-
|
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
|
720
|
-
|
721
|
-
|
722
|
-
|
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
|
-
|
768
|
+
if is_android?
|
769
|
+
files << android_test_server
|
770
|
+
end
|
771
|
+
|
772
|
+
files
|
773
|
+
end
|
733
774
|
|
734
|
-
|
735
|
-
|
736
|
-
|
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
|
-
|
740
|
-
|
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
|
-
|
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
|
-
|
747
|
-
|
748
|
-
|
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
|
-
|
801
|
-
|
802
|
-
|
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
|
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?
|
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
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
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
|
-
|
929
|
-
|
930
|
-
|
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
|
-
|
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
|
-
|
990
|
-
|
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
|