xamarin-test-cloud 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGES.txt +2 -0
- data/README.md +0 -0
- data/bin/test-cloud +18 -0
- data/lib/GemfileAndroid +3 -0
- data/lib/GemfileIOS +3 -0
- data/lib/xamarin-test-cloud/cli.rb +618 -0
- data/lib/xamarin-test-cloud/version.rb +3 -0
- data/test/ipa/features/step_definitions/calabash_steps.rb +1 -0
- data/test/ipa/features/step_definitions/my_first_steps.rb +4 -0
- data/test/ipa/features/support/env.rb +1 -0
- data/test/ipa/features/support/hooks.rb +0 -0
- data/test/ipa/features/support/launch.rb +77 -0
- data/test/test_parser.rb +62 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: eb0063f60324d962136569cbb02a120355c4a9b3
|
4
|
+
data.tar.gz: c3f3a4eafe87aa4ee3a2e750cdd92144ac50b86c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 94064d3747ab099c6f042106f6bb93ad69d4bb1bba70cdef9223be64539d48334bf1a66aac47d4457963823d09990e36e6b04df7d38c1ea4c0a4d1652d20fa7f
|
7
|
+
data.tar.gz: f0c598d9d96dc52303cb2b3e52425aa3bc1cf3b794fd7f3abbf5ee9a89c022ee143da830abef93711eb3b85b27bacad34976419a9221de3bda3fa17310712e7a
|
data/CHANGES.txt
ADDED
data/README.md
ADDED
File without changes
|
data/bin/test-cloud
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'xamarin-test-cloud/cli'
|
3
|
+
|
4
|
+
begin
|
5
|
+
ENV['THOR_DEBUG'] = '1'
|
6
|
+
XamarinTestCloud::CLI.start
|
7
|
+
exit(true)
|
8
|
+
rescue XamarinTestCloud::ValidationError, Thor::RequiredArgumentMissingError, Thor::UndefinedCommandError => e
|
9
|
+
puts e.message
|
10
|
+
exit 64
|
11
|
+
rescue Thor::Error => e
|
12
|
+
puts e.message
|
13
|
+
if ENV['DEBUG'] == '1'
|
14
|
+
puts e.backtrace.join("\n") if e.backtrace
|
15
|
+
p e.class
|
16
|
+
end
|
17
|
+
exit 70
|
18
|
+
end
|
data/lib/GemfileAndroid
ADDED
data/lib/GemfileIOS
ADDED
@@ -0,0 +1,618 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'yaml'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'zip/zip'
|
5
|
+
require 'digest'
|
6
|
+
require 'rest_client'
|
7
|
+
require 'json'
|
8
|
+
require 'rbconfig'
|
9
|
+
require 'tmpdir'
|
10
|
+
require 'fileutils'
|
11
|
+
|
12
|
+
module XamarinTestCloud
|
13
|
+
|
14
|
+
class ValidationError < Thor::InvocationError
|
15
|
+
end
|
16
|
+
|
17
|
+
class CLI < Thor
|
18
|
+
include Thor::Actions
|
19
|
+
|
20
|
+
attr_accessor :host, :app, :api_key, :appname, :workspace, :config, :profile, :features_zip, :skip_check, :dry_run, :device_selection
|
21
|
+
attr_accessor :pretty
|
22
|
+
attr_accessor :endpoint_path
|
23
|
+
|
24
|
+
FILE_UPLOAD_ENDPOINT = 'upload2'
|
25
|
+
FORM_URL_ENCODED_ENDPOINT = 'upload'
|
26
|
+
|
27
|
+
|
28
|
+
def self.source_root
|
29
|
+
File.join(File.dirname(__FILE__), '..')
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'version', 'Prints version of the xamarin-test-cloud gem'
|
33
|
+
|
34
|
+
def version
|
35
|
+
puts XamarinTestCloud::VERSION
|
36
|
+
end
|
37
|
+
|
38
|
+
desc 'submit <APP> <API_KEY>', 'Submits your app and test suite to Xamarin Test Cloud'
|
39
|
+
|
40
|
+
method_option :host,
|
41
|
+
:desc => 'Test Cloud host to submit to',
|
42
|
+
:aliases => '-h', :type => :string,
|
43
|
+
:default => (ENV['XTC_ENDPOINT'] || 'https://testcloud.xamarin.com/ci')
|
44
|
+
|
45
|
+
method_option 'app-name',
|
46
|
+
:desc => 'App name to create or add test to',
|
47
|
+
:aliases => '-a',
|
48
|
+
:required => true,
|
49
|
+
:type => :string
|
50
|
+
|
51
|
+
method_option 'device-selection',
|
52
|
+
:desc => 'Device selection',
|
53
|
+
:aliases => '-d',
|
54
|
+
:required => true,
|
55
|
+
:type => :string
|
56
|
+
|
57
|
+
method_option :workspace,
|
58
|
+
:desc => 'Workspace containing Gemfile and features',
|
59
|
+
:aliases => '-w',
|
60
|
+
:type => :string,
|
61
|
+
:default => File.expand_path('.')
|
62
|
+
|
63
|
+
method_option :features,
|
64
|
+
:desc => 'Zip file with features, step definitions, etc...',
|
65
|
+
:aliases => '-f',
|
66
|
+
:type => :string
|
67
|
+
|
68
|
+
method_option :config,
|
69
|
+
:desc => 'Cucumber configuration file (cucumber.yml)',
|
70
|
+
:aliases => '-c',
|
71
|
+
:type => :string
|
72
|
+
|
73
|
+
method_option :profile,
|
74
|
+
:desc => 'Profile to run (profile from cucumber.yml)',
|
75
|
+
:aliases => '-p',
|
76
|
+
:type => :string
|
77
|
+
|
78
|
+
method_option :pretty,
|
79
|
+
:desc => 'Pretty print output.',
|
80
|
+
:type => :boolean,
|
81
|
+
:default => false
|
82
|
+
|
83
|
+
method_option 'skip-check',
|
84
|
+
:desc => 'Skip checking for ipa linked with Calabash (iOS only)',
|
85
|
+
:type => :boolean
|
86
|
+
|
87
|
+
method_option 'dry-run',
|
88
|
+
:desc => "Sanity check only, don't upload",
|
89
|
+
:aliases => '-d',
|
90
|
+
:type => :boolean,
|
91
|
+
:default => false #do upload by default
|
92
|
+
|
93
|
+
def submit(app, api_key)
|
94
|
+
|
95
|
+
self.host = options[:host]
|
96
|
+
|
97
|
+
self.pretty = options[:pretty]
|
98
|
+
|
99
|
+
app_path = File.expand_path(app)
|
100
|
+
unless File.exist?(app_path)
|
101
|
+
raise ValidationError, "App is not a file: #{app_path}"
|
102
|
+
end
|
103
|
+
|
104
|
+
self.app = app_path
|
105
|
+
|
106
|
+
self.dry_run = options['dry-run']
|
107
|
+
|
108
|
+
self.api_key = api_key
|
109
|
+
|
110
|
+
self.appname = options['app-name']
|
111
|
+
|
112
|
+
self.device_selection = options['device-selection']
|
113
|
+
unless File.exist?(device_selection)
|
114
|
+
raise ValidationError, "Device selection is not a file: #{device_selection}"
|
115
|
+
end
|
116
|
+
|
117
|
+
self.skip_check = options['skip-check']
|
118
|
+
|
119
|
+
parse_and_set_config_and_profile
|
120
|
+
|
121
|
+
workspace_path = options[:workspace] || File.expand_path('.')
|
122
|
+
|
123
|
+
unless File.directory?(workspace_path)
|
124
|
+
raise ValidationError, "Provided workspace: #{workspace_path} is not a directory."
|
125
|
+
end
|
126
|
+
self.workspace = File.join(File.expand_path(workspace_path), File::Separator)
|
127
|
+
|
128
|
+
features_path = options[:features]
|
129
|
+
|
130
|
+
unless features_path.nil?
|
131
|
+
if File.exist?(features_path)
|
132
|
+
self.features_zip = File.expand_path(features_path)
|
133
|
+
else
|
134
|
+
raise ValidationError, "Provided features file does not exist #{features_path}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
if ENV['DEBUG']
|
139
|
+
puts "Host = #{self.host}"
|
140
|
+
puts "App = #{self.app}"
|
141
|
+
puts "App Name = #{self.app}"
|
142
|
+
puts "API Key = #{self.api_key}"
|
143
|
+
puts "Device Selection = #{self.device_selection}"
|
144
|
+
puts "Workspace = #{self.workspace}"
|
145
|
+
puts "Features Zip = #{self.features_zip}"
|
146
|
+
puts "Config = #{self.config}"
|
147
|
+
puts "Profile = #{self.profile}"
|
148
|
+
puts "Skip Check = #{self.skip_check}"
|
149
|
+
end
|
150
|
+
|
151
|
+
#Argument parsing done
|
152
|
+
json = submit_test_job[:body]
|
153
|
+
log_header("Test enqueued")
|
154
|
+
puts "User: #{json["user_email"]}"
|
155
|
+
|
156
|
+
puts "Devices:"
|
157
|
+
json["devices"].each do |device|
|
158
|
+
puts device
|
159
|
+
end
|
160
|
+
puts ""
|
161
|
+
|
162
|
+
|
163
|
+
result = wait_for_job(json["id"])
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
default_task :submit
|
168
|
+
|
169
|
+
no_tasks do
|
170
|
+
|
171
|
+
def exit_on_failure?
|
172
|
+
true
|
173
|
+
end
|
174
|
+
|
175
|
+
def wait_for_job(id)
|
176
|
+
while(true)
|
177
|
+
status_json = JSON.parse(http_post("status", {'id' => id, 'api_key' => api_key}))
|
178
|
+
|
179
|
+
log "Status: #{status_json["status_description"]}"
|
180
|
+
if status_json["status"] == "finished"
|
181
|
+
puts "Done!"
|
182
|
+
exit 0
|
183
|
+
end
|
184
|
+
|
185
|
+
if ["cancelled", "invalid"].include?(status_json["status"])
|
186
|
+
puts "Failed!"
|
187
|
+
exit 1
|
188
|
+
end
|
189
|
+
sleep 10
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
def submit_test_job
|
195
|
+
start_at = Time.now
|
196
|
+
|
197
|
+
server = verify_app_and_extract_test_server
|
198
|
+
|
199
|
+
|
200
|
+
log_header('Checking for Gemfile')
|
201
|
+
gemfile_path = File.join(self.workspace, 'Gemfile')
|
202
|
+
unless File.exist?(gemfile_path)
|
203
|
+
copy_default_gemfile(gemfile_path, server)
|
204
|
+
end
|
205
|
+
|
206
|
+
log_header('Packaging')
|
207
|
+
FileUtils.cd(self.workspace) do
|
208
|
+
unless system('bundle package --all')
|
209
|
+
log_and_abort 'Bundler failed. Please check command: bundle package'
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
log_header('Verifying dependencies')
|
215
|
+
verify_dependencies
|
216
|
+
|
217
|
+
|
218
|
+
if dry_run
|
219
|
+
log_header('Dry run only')
|
220
|
+
log('Dry run completed OK')
|
221
|
+
return
|
222
|
+
end
|
223
|
+
|
224
|
+
app_file, files, paths = gather_files_and_paths_to_upload(all_files)
|
225
|
+
|
226
|
+
|
227
|
+
log_header('Uploading negotiated files')
|
228
|
+
|
229
|
+
upload_data = {'files' => files,
|
230
|
+
'paths' => paths,
|
231
|
+
'app_file' => app_file,
|
232
|
+
'device_selection' => IO.read(device_selection),
|
233
|
+
'app' => self.appname,
|
234
|
+
'api_key' => api_key,
|
235
|
+
'app_filename' => File.basename(app)}
|
236
|
+
|
237
|
+
if profile #only if config and profile
|
238
|
+
upload_data['profile'] = profile
|
239
|
+
end
|
240
|
+
|
241
|
+
if ENV['DEBUG']
|
242
|
+
puts JSON.pretty_generate(upload_data)
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
contains_file = files.find { |f| f.is_a?(File) }
|
247
|
+
|
248
|
+
contains_file = contains_file || app_file.is_a?(File)
|
249
|
+
|
250
|
+
if contains_file
|
251
|
+
self.endpoint_path = FILE_UPLOAD_ENDPOINT #nginx receives upload
|
252
|
+
else
|
253
|
+
self.endpoint_path = FORM_URL_ENCODED_ENDPOINT #ruby receives upload
|
254
|
+
end
|
255
|
+
|
256
|
+
if ENV['DEBUG']
|
257
|
+
puts "Will upload to file path: #{self.endpoint_path}"
|
258
|
+
end
|
259
|
+
|
260
|
+
|
261
|
+
response = http_post(endpoint_path, upload_data) do |response, request, result, &block|
|
262
|
+
if ENV['DEBUG']
|
263
|
+
puts "Request url: #{request.url}"
|
264
|
+
puts "Response code: #{response.code}"
|
265
|
+
end
|
266
|
+
case response.code
|
267
|
+
when 200..202
|
268
|
+
response
|
269
|
+
when 403
|
270
|
+
abort do
|
271
|
+
puts 'Invalid API key'
|
272
|
+
end
|
273
|
+
when 413
|
274
|
+
abort do
|
275
|
+
puts 'Files too large'
|
276
|
+
end
|
277
|
+
else
|
278
|
+
abort do
|
279
|
+
log 'Unexpected Error. Please contact support at testcloud@xamarin.com.'
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
return :status => response.code, :body => JSON.parse(response)
|
285
|
+
end
|
286
|
+
|
287
|
+
|
288
|
+
def copy_default_gemfile(gemfile_path, server)
|
289
|
+
log('')
|
290
|
+
log('Gemfile missing.')
|
291
|
+
log('You must provide a Gemfile in your workspace.')
|
292
|
+
log('A Gemfile must describe your dependencies and their versions')
|
293
|
+
log('See: http://gembundler.com/v1.3/gemfile.html')
|
294
|
+
log('')
|
295
|
+
log('Warning proceeding with default Gemfile.')
|
296
|
+
log('It is strongly recommended that you create a custom Gemfile.')
|
297
|
+
tgt = nil
|
298
|
+
if is_android?
|
299
|
+
log('Creating Gemfile for Android')
|
300
|
+
tgt = File.join(CLI.source_root, 'GemfileAndroid')
|
301
|
+
elsif is_ios?
|
302
|
+
log('Creating Gemfile for iOS')
|
303
|
+
gemfile = 'GemfileIOS'
|
304
|
+
if server == :frank
|
305
|
+
raise ValidationError, 'Frank not supported just yet'
|
306
|
+
end
|
307
|
+
tgt = File.join(CLI.source_root, gemfile)
|
308
|
+
else
|
309
|
+
raise ValidationError, 'Your app must be an ipa or apk file.'
|
310
|
+
end
|
311
|
+
log("Proceeding with Gemfile: #{gemfile_path}")
|
312
|
+
|
313
|
+
|
314
|
+
FileUtils.cp(File.expand_path(tgt), gemfile_path)
|
315
|
+
|
316
|
+
puts(File.read(gemfile_path))
|
317
|
+
|
318
|
+
log('')
|
319
|
+
|
320
|
+
end
|
321
|
+
|
322
|
+
def gather_files_and_paths_to_upload(collected_files)
|
323
|
+
|
324
|
+
log_header('Calculating digests')
|
325
|
+
|
326
|
+
file_paths = collected_files[:files]
|
327
|
+
feature_prefix = collected_files[:feature_prefix]
|
328
|
+
workspace_prefix = collected_files[:workspace_prefix]
|
329
|
+
|
330
|
+
hashes = file_paths.collect { |f| digest(f) }
|
331
|
+
hashes << digest(app)
|
332
|
+
|
333
|
+
|
334
|
+
log_header('Negotiating upload')
|
335
|
+
|
336
|
+
response = http_post('check_hash', {'hashes' => hashes})
|
337
|
+
|
338
|
+
cache_status = JSON.parse(response)
|
339
|
+
|
340
|
+
log_header('Gathering files based on negotiation')
|
341
|
+
|
342
|
+
files = []
|
343
|
+
paths = []
|
344
|
+
file_paths.each do |file|
|
345
|
+
if cache_status[digest(file)]
|
346
|
+
#Server already knows about this file. No need to upload it.
|
347
|
+
files << digest(file)
|
348
|
+
else
|
349
|
+
#Upload file
|
350
|
+
files << File.new(file)
|
351
|
+
end
|
352
|
+
|
353
|
+
if file.start_with?(feature_prefix)
|
354
|
+
prefix = feature_prefix
|
355
|
+
else
|
356
|
+
prefix = workspace_prefix
|
357
|
+
end
|
358
|
+
paths << file.sub(prefix, '')
|
359
|
+
end
|
360
|
+
|
361
|
+
if config
|
362
|
+
files << File.new(config)
|
363
|
+
paths << 'config/cucumber.yml'
|
364
|
+
end
|
365
|
+
|
366
|
+
app_file = cache_status[digest(app)] ? digest(app) : File.new(app)
|
367
|
+
|
368
|
+
return app_file, files, paths
|
369
|
+
end
|
370
|
+
|
371
|
+
def digest(file)
|
372
|
+
Digest::SHA256.file(file).hexdigest
|
373
|
+
end
|
374
|
+
|
375
|
+
def unzip_file (file, destination)
|
376
|
+
Zip::ZipFile.open(file) { |zip_file|
|
377
|
+
zip_file.each { |f|
|
378
|
+
f_path=File.join(destination, f.name)
|
379
|
+
FileUtils.mkdir_p(File.dirname(f_path))
|
380
|
+
zip_file.extract(f, f_path) unless File.exist?(f_path)
|
381
|
+
}
|
382
|
+
}
|
383
|
+
end
|
384
|
+
|
385
|
+
|
386
|
+
def is_android?
|
387
|
+
app.end_with? '.apk'
|
388
|
+
end
|
389
|
+
|
390
|
+
def calabash_android_version
|
391
|
+
`bundle exec calabash-android version`.strip
|
392
|
+
end
|
393
|
+
|
394
|
+
def is_ios?
|
395
|
+
app.end_with? '.ipa'
|
396
|
+
end
|
397
|
+
|
398
|
+
def test_server_path
|
399
|
+
require 'digest/md5'
|
400
|
+
digest = Digest::MD5.file(app).hexdigest
|
401
|
+
File.join('test_servers', "#{digest}_#{calabash_android_version}.apk")
|
402
|
+
end
|
403
|
+
|
404
|
+
def all_files
|
405
|
+
dir = workspace
|
406
|
+
if features_zip
|
407
|
+
dir = Dir.mktmpdir
|
408
|
+
unzip_file(features_zip, dir)
|
409
|
+
dir = File.join(dir, File::Separator)
|
410
|
+
end
|
411
|
+
|
412
|
+
|
413
|
+
files = Dir.glob(File.join("#{dir}features", '**', '*'))
|
414
|
+
|
415
|
+
if File.directory?("#{dir}playback")
|
416
|
+
files += Dir.glob(File.join("#{dir}playback", '*'))
|
417
|
+
end
|
418
|
+
|
419
|
+
if config
|
420
|
+
files << config
|
421
|
+
end
|
422
|
+
|
423
|
+
|
424
|
+
files += Dir.glob(File.join("#{workspace}vendor", 'cache', '*'))
|
425
|
+
|
426
|
+
if workspace and workspace.strip != ''
|
427
|
+
files += Dir.glob("#{workspace}Gemfile")
|
428
|
+
files += Dir.glob("#{workspace}Gemfile.lock")
|
429
|
+
end
|
430
|
+
|
431
|
+
if is_android?
|
432
|
+
files << test_server_path
|
433
|
+
end
|
434
|
+
|
435
|
+
{:feature_prefix => dir, :workspace_prefix => workspace, :files => files.find_all { |file_or_dir| File.file? file_or_dir }}
|
436
|
+
end
|
437
|
+
|
438
|
+
def http_post(address, args, &block)
|
439
|
+
exec_options = {}
|
440
|
+
if ENV['XTC_USERNAME'] && ENV['XTC_PASSWORD']
|
441
|
+
exec_options[:user] = ENV['XTC_USERNAME']
|
442
|
+
exec_options[:password] = ENV['XTC_PASSWORD']
|
443
|
+
end
|
444
|
+
|
445
|
+
if block_given?
|
446
|
+
exec_options = exec_options.merge ({:method => :post, :url => "#{host}/#{address}", :payload => args,
|
447
|
+
:timeout => 90000000,
|
448
|
+
:open_timeout => 90000000,
|
449
|
+
:headers => {:content_type => 'multipart/form-data'}})
|
450
|
+
response = RestClient::Request.execute(exec_options) do |response, request, result, &other_block|
|
451
|
+
block.call(response, request, result, &other_block)
|
452
|
+
end
|
453
|
+
else
|
454
|
+
exec_options = exec_options.merge(:method => :post, :url => "#{host}/#{address}", :payload => args)
|
455
|
+
response = RestClient::Request.execute(exec_options)
|
456
|
+
end
|
457
|
+
|
458
|
+
response.body
|
459
|
+
end
|
460
|
+
|
461
|
+
def is_windows?
|
462
|
+
(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
|
463
|
+
end
|
464
|
+
|
465
|
+
def is_macosx?
|
466
|
+
(RbConfig::CONFIG['host_os'] =~ /darwin/)
|
467
|
+
end
|
468
|
+
|
469
|
+
def validate_ipa(ipa)
|
470
|
+
result = false
|
471
|
+
dir = Dir.mktmpdir #do |dir|
|
472
|
+
|
473
|
+
unzip_file(ipa, dir)
|
474
|
+
unless File.directory?("#{dir}/Payload") #macos only
|
475
|
+
raise ValidationError, "Unzipping #{ipa} to #{dir} failed: Did not find a Payload directory (invalid .ipa)."
|
476
|
+
end
|
477
|
+
app_dir = Dir.foreach("#{dir}/Payload").find { |d| /\.app$/.match(d) }
|
478
|
+
app = app_dir.split('.')[0]
|
479
|
+
res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/#{app}" -o 2> /dev/null | grep CalabashServer`
|
480
|
+
|
481
|
+
if /CalabashServer/.match(res)
|
482
|
+
puts "ipa: #{ipa} *contains* calabash.framework"
|
483
|
+
result = :calabash
|
484
|
+
end
|
485
|
+
|
486
|
+
unless result
|
487
|
+
res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/#{app}" -o 2> /dev/null | grep FrankServer`
|
488
|
+
if /FrankServer/.match(res)
|
489
|
+
puts "ipa: #{ipa} *contains* FrankServer"
|
490
|
+
result = :frank
|
491
|
+
else
|
492
|
+
puts "ipa: #{ipa} *does not contain* calabash.framework"
|
493
|
+
result = false
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
result
|
498
|
+
end
|
499
|
+
|
500
|
+
|
501
|
+
def log(message)
|
502
|
+
puts "#{Time.now } #{message}"
|
503
|
+
$stdout.flush
|
504
|
+
end
|
505
|
+
|
506
|
+
def log_header(message)
|
507
|
+
if is_windows?
|
508
|
+
puts "\n### #{message} ###"
|
509
|
+
else
|
510
|
+
puts "\n\e[#{35}m### #{message} ###\e[0m"
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
514
|
+
|
515
|
+
def verify_app_and_extract_test_server
|
516
|
+
server = nil
|
517
|
+
|
518
|
+
unless File.exist?(app)
|
519
|
+
raise ValidationError, "No such file: #{app}"
|
520
|
+
end
|
521
|
+
unless (/\.(apk|ipa)$/ =~ app)
|
522
|
+
raise ValidationError, '<APP> should be either an ipa or apk file.'
|
523
|
+
end
|
524
|
+
if is_ios? and is_macosx? and not skip_check
|
525
|
+
log_header('Checking ipa for linking with Calabash')
|
526
|
+
server = validate_ipa(app)
|
527
|
+
abort_unless(server) do
|
528
|
+
puts 'The .ipa file does not seem to be linked with Calabash.'
|
529
|
+
puts 'Verify that your app is linked correctly.'
|
530
|
+
puts 'To disable this check run with --skip-check or set Environment Variable CHECK_IPA=0'
|
531
|
+
end
|
532
|
+
end
|
533
|
+
server
|
534
|
+
end
|
535
|
+
|
536
|
+
def abort(&block)
|
537
|
+
yield block
|
538
|
+
exit 1
|
539
|
+
end
|
540
|
+
|
541
|
+
def abort_unless(condition, &block)
|
542
|
+
unless condition
|
543
|
+
yield block
|
544
|
+
exit 1
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
def log_and_abort(message)
|
549
|
+
abort do
|
550
|
+
puts message
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
|
555
|
+
def verify_dependencies
|
556
|
+
if is_android?
|
557
|
+
abort_unless(File.exist?(test_server_path)) do
|
558
|
+
puts 'No test server found. Please run:'
|
559
|
+
puts " calabash-android build #{app}"
|
560
|
+
end
|
561
|
+
|
562
|
+
calabash_gem = Dir.glob('vendor/cache/calabash-android-*').first
|
563
|
+
abort_unless(calabash_gem) do
|
564
|
+
puts 'calabash-android was not packaged correct.'
|
565
|
+
puts 'Please tell testcloud@xamarin.com about this bug.'
|
566
|
+
end
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
|
571
|
+
def parse_and_set_config_and_profile
|
572
|
+
config_path = options[:config]
|
573
|
+
if config_path
|
574
|
+
config_path = File.expand_path(config_path)
|
575
|
+
unless File.exist?(config_path)
|
576
|
+
raise ValidationError, "Config file does not exist #{config_path}"
|
577
|
+
end
|
578
|
+
|
579
|
+
|
580
|
+
begin
|
581
|
+
config_yml = YAML.load_file(config_path)
|
582
|
+
rescue Exception => e
|
583
|
+
puts "Unable to parse #{config_path} as yml. Is this your Cucumber.yml file?"
|
584
|
+
raise ValidationError, e
|
585
|
+
end
|
586
|
+
|
587
|
+
if ENV['DEBUG']
|
588
|
+
puts 'Parsed Cucumber config as:'
|
589
|
+
puts config_yml.inspect
|
590
|
+
end
|
591
|
+
|
592
|
+
profile = options[:profile]
|
593
|
+
unless profile
|
594
|
+
raise ValidationError, 'Config file provided but no profile selected.'
|
595
|
+
else
|
596
|
+
unless config_yml[profile]
|
597
|
+
raise ValidationError, "Config file provided did not contain profile #{profile}."
|
598
|
+
else
|
599
|
+
puts "Using profile #{profile}..."
|
600
|
+
self.profile = profile
|
601
|
+
end
|
602
|
+
end
|
603
|
+
else
|
604
|
+
if options[:profile]
|
605
|
+
raise ValidationError, 'Profile selected but no config file provided.'
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
self.config = config_path
|
610
|
+
end
|
611
|
+
|
612
|
+
end
|
613
|
+
|
614
|
+
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'calabash-cucumber/calabash_steps'
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'calabash-cucumber/cucumber'
|
File without changes
|
@@ -0,0 +1,77 @@
|
|
1
|
+
########################################
|
2
|
+
# #
|
3
|
+
# Important Note #
|
4
|
+
# #
|
5
|
+
# When running calabash-ios tests at #
|
6
|
+
# www.xamarin-test-cloud.com #
|
7
|
+
# this file will be overwritten by #
|
8
|
+
# a file which automates #
|
9
|
+
# app launch on devices. #
|
10
|
+
# #
|
11
|
+
# Don't rely on this file being #
|
12
|
+
# present when running at #
|
13
|
+
# www.xamarin-test-cloud.com. #
|
14
|
+
# #
|
15
|
+
# Only put stuff here to automate #
|
16
|
+
# iOS Simulator. #
|
17
|
+
# #
|
18
|
+
# You can put your app bundle path #
|
19
|
+
# for automating simulator app start: #
|
20
|
+
# Uncomment APP_BUNDLE_PATH =.. #
|
21
|
+
# #
|
22
|
+
########################################
|
23
|
+
|
24
|
+
require 'calabash-cucumber/launch/simulator_helper'
|
25
|
+
require 'sim_launcher'
|
26
|
+
|
27
|
+
# Uncomment and replace ?? appropriately
|
28
|
+
# This should point to your Simulator build
|
29
|
+
# which includes calabash framework
|
30
|
+
# this is usually the Calabash build configuration
|
31
|
+
# of your production target.
|
32
|
+
#APP_BUNDLE_PATH = "~/Library/Developer/Xcode/DerivedData/??/Build/Products/Calabash-iphonesimulator/??.app"
|
33
|
+
#
|
34
|
+
|
35
|
+
def reset_app_jail(sdk, app_path)
|
36
|
+
app = File.basename(app_path)
|
37
|
+
bundle = `find "#{ENV['HOME']}/Library/Application Support/iPhone Simulator/#{sdk}/Applications/" -type d -depth 2 -name #{app} | head -n 1`
|
38
|
+
return if bundle.empty? # Assuming we're already clean
|
39
|
+
|
40
|
+
sandbox = File.dirname(bundle)
|
41
|
+
['Library', 'Documents', 'tmp'].each do |dir|
|
42
|
+
FileUtils.rm_rf(File.join(sandbox, dir))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def relaunch
|
47
|
+
if ENV['NO_LAUNCH']!="1"
|
48
|
+
sdk = ENV['SDK_VERSION'] || SimLauncher::SdkDetector.new().latest_sdk_version
|
49
|
+
path = Calabash::Cucumber::SimulatorHelper.app_bundle_or_raise(app_path)
|
50
|
+
if ENV['RESET_BETWEEN_SCENARIOS']=="1"
|
51
|
+
reset_app_jail(sdk, path)
|
52
|
+
end
|
53
|
+
|
54
|
+
Calabash::Cucumber::SimulatorHelper.relaunch(path,sdk,ENV['DEVICE'] || 'iphone')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def app_path
|
59
|
+
ENV['APP_BUNDLE_PATH'] || (defined?(APP_BUNDLE_PATH) && APP_BUNDLE_PATH)
|
60
|
+
end
|
61
|
+
|
62
|
+
def calabash_notify
|
63
|
+
if self.respond_to?(:on_launch)
|
64
|
+
self.on_launch
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
Before do |scenario|
|
69
|
+
relaunch
|
70
|
+
calabash_notify
|
71
|
+
end
|
72
|
+
|
73
|
+
at_exit do
|
74
|
+
if ENV['NO_LAUNCH']!="1" and ENV['NO_STOP']!="1"
|
75
|
+
Calabash::Cucumber::SimulatorHelper.stop
|
76
|
+
end
|
77
|
+
end
|
data/test/test_parser.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'thor'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'xamarin-test-cloud/cli'
|
5
|
+
|
6
|
+
class TestParser < Test::Unit::TestCase
|
7
|
+
def test_should_raise_if_no_app_or_api_key_is_given
|
8
|
+
script = XamarinTestCloud::CLI.new([])
|
9
|
+
assert_raise Thor::InvocationError do
|
10
|
+
script.invoke(:submit)
|
11
|
+
end
|
12
|
+
|
13
|
+
script = XamarinTestCloud::CLI.new(["test/ipa/2012 Olympics_cal.ipa"])
|
14
|
+
assert_raise Thor::InvocationError do
|
15
|
+
script.invoke(:submit)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_should_raise_if_app_is_not_file_ipa_or_apk
|
22
|
+
script = XamarinTestCloud::CLI.new(["test/ipa/NONE_EXIST_2012 Olympics_cal.ipa","JIFZCTPZJJXJLEKMMYRY","."])
|
23
|
+
assert_raise RuntimeError do
|
24
|
+
script.invoke(:submit)
|
25
|
+
end
|
26
|
+
|
27
|
+
script = XamarinTestCloud::CLI.new(["test/ipa/features.zip","JIFZCTPZJJXJLEKMMYRY","."])
|
28
|
+
|
29
|
+
assert_raise RuntimeError do
|
30
|
+
script.invoke(:submit)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_should_parse_all_configuration_options
|
35
|
+
FileUtils.rm_f(File.join("test","vendor"))
|
36
|
+
FileUtils.rm_f(File.join("test","Gemfile"))
|
37
|
+
FileUtils.rm_f(File.join("test","Gemfile.lock"))
|
38
|
+
|
39
|
+
config_options = {
|
40
|
+
:host => "http://localhost:8080",
|
41
|
+
:workspace => "test",
|
42
|
+
:features => "test/ipa/features.zip",
|
43
|
+
:config => "test/config/cucumber.yml",
|
44
|
+
:profile => "a",
|
45
|
+
"skip-check" => false,
|
46
|
+
"reset-between-scenarios" => false,
|
47
|
+
"dry-run" => true
|
48
|
+
}
|
49
|
+
script = XamarinTestCloud::CLI.new(["test/ipa/2012 Olympics_cal.ipa","JIFZCTPZJJXJLEKMMYRY"],config_options)
|
50
|
+
|
51
|
+
|
52
|
+
script.invoke(:submit)
|
53
|
+
|
54
|
+
FileUtils.rm_f(File.join("test","vendor","cache"))
|
55
|
+
FileUtils.rm_f(File.join("test","Gemfile"))
|
56
|
+
FileUtils.rm_f(File.join("test","Gemfile.lock"))
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xamarin-test-cloud
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Karl Krukow
|
8
|
+
- Jonas Maturana Larsen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thor
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 0.18.1
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - '>='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 0.18.1
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: bundler
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 1.3.0
|
35
|
+
- - <
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '2.0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 1.3.0
|
45
|
+
- - <
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.0'
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: json
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rubyzip
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.9.9
|
69
|
+
type: :runtime
|
70
|
+
prerelease: false
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.9.9
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: rest-client
|
78
|
+
requirement: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.6.7
|
83
|
+
type: :runtime
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.6.7
|
90
|
+
description: Xamarin Test Cloud let's you automatically test your app on hundreds
|
91
|
+
of mobile devices
|
92
|
+
email:
|
93
|
+
- karl@xamarin.com
|
94
|
+
- jonas@xamarin.com
|
95
|
+
executables:
|
96
|
+
- test-cloud
|
97
|
+
extensions: []
|
98
|
+
extra_rdoc_files: []
|
99
|
+
files:
|
100
|
+
- bin/test-cloud
|
101
|
+
- lib/GemfileAndroid
|
102
|
+
- lib/GemfileIOS
|
103
|
+
- lib/xamarin-test-cloud/cli.rb
|
104
|
+
- lib/xamarin-test-cloud/version.rb
|
105
|
+
- README.md
|
106
|
+
- CHANGES.txt
|
107
|
+
- test/ipa/features/step_definitions/calabash_steps.rb
|
108
|
+
- test/ipa/features/step_definitions/my_first_steps.rb
|
109
|
+
- test/ipa/features/support/env.rb
|
110
|
+
- test/ipa/features/support/hooks.rb
|
111
|
+
- test/ipa/features/support/launch.rb
|
112
|
+
- test/test_parser.rb
|
113
|
+
homepage: http://xamarin.com/test-cloud
|
114
|
+
licenses: []
|
115
|
+
metadata: {}
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
requirements: []
|
131
|
+
rubyforge_project:
|
132
|
+
rubygems_version: 2.0.5
|
133
|
+
signing_key:
|
134
|
+
specification_version: 4
|
135
|
+
summary: Command-line interface to Xamarin Test Cloud
|
136
|
+
test_files:
|
137
|
+
- test/ipa/features/step_definitions/calabash_steps.rb
|
138
|
+
- test/ipa/features/step_definitions/my_first_steps.rb
|
139
|
+
- test/ipa/features/support/env.rb
|
140
|
+
- test/ipa/features/support/hooks.rb
|
141
|
+
- test/ipa/features/support/launch.rb
|
142
|
+
- test/test_parser.rb
|