lesspainful 0.9.14 → 0.10.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.
- data/Gemfile.lock +5 -3
- data/Rakefile +9 -0
- data/bin/lesspainful +17 -330
- data/lesspainful.gemspec +4 -2
- data/lib/lesspainful/cli.rb +547 -0
- data/lib/version.rb +1 -1
- data/test/config/cucumber.yml +1 -0
- data/test/test_parser.rb +62 -0
- metadata +36 -11
data/Gemfile.lock
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
lesspainful (0.
|
4
|
+
lesspainful (0.10.0)
|
5
5
|
bundler (~> 1.2)
|
6
6
|
json
|
7
|
-
rest-client
|
8
|
-
rubyzip
|
7
|
+
rest-client (= 1.6.7)
|
8
|
+
rubyzip (= 0.9.9)
|
9
|
+
thor (= 0.17.0)
|
9
10
|
|
10
11
|
GEM
|
11
12
|
remote: http://rubygems.org/
|
@@ -15,6 +16,7 @@ GEM
|
|
15
16
|
rest-client (1.6.7)
|
16
17
|
mime-types (>= 1.16)
|
17
18
|
rubyzip (0.9.9)
|
19
|
+
thor (0.17.0)
|
18
20
|
|
19
21
|
PLATFORMS
|
20
22
|
ruby
|
data/Rakefile
CHANGED
data/bin/lesspainful
CHANGED
@@ -1,339 +1,26 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
require 'rubygems'
|
3
|
-
require 'zip/zip'
|
4
|
-
require 'digest'
|
5
|
-
require 'rest_client'
|
6
|
-
require 'json'
|
7
|
-
require 'rbconfig'
|
8
|
-
require 'tmpdir'
|
9
|
-
|
10
|
-
def host
|
11
|
-
ENV["LP_HOST"] || "https://www.lesspainful.com"
|
12
|
-
end
|
13
|
-
|
14
|
-
def digest(file)
|
15
|
-
Digest::SHA256.file(file).hexdigest
|
16
|
-
end
|
17
|
-
|
18
|
-
def unzip_file (file, destination)
|
19
|
-
Zip::ZipFile.open(file) { |zip_file|
|
20
|
-
zip_file.each { |f|
|
21
|
-
f_path=File.join(destination, f.name)
|
22
|
-
FileUtils.mkdir_p(File.dirname(f_path))
|
23
|
-
zip_file.extract(f, f_path) unless File.exist?(f_path)
|
24
|
-
}
|
25
|
-
}
|
26
|
-
end
|
27
|
-
|
28
|
-
def workspace
|
29
|
-
if ARGV[2]
|
30
|
-
abort_unless(File.exist?(ARGV[2])) do
|
31
|
-
puts "Provided workspace: #{ARGV[2]} does not exist."
|
32
|
-
end
|
33
|
-
File.join(File.expand_path(ARGV[2]),File::Separator)
|
34
|
-
else
|
35
|
-
""
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def features_zip
|
40
|
-
if ARGV[3] and ARGV[3].end_with?".zip"
|
41
|
-
abort_unless(File.exist?(ARGV[3])) do
|
42
|
-
puts "No file found #{ARGV[3]}"
|
43
|
-
end
|
44
|
-
File.expand_path(ARGV[3])
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def app
|
49
|
-
ARGV[0]
|
50
|
-
end
|
51
|
-
|
52
|
-
def api_key
|
53
|
-
ARGV[1]
|
54
|
-
end
|
55
|
-
|
56
|
-
def is_android?
|
57
|
-
app.end_with? ".apk"
|
58
|
-
end
|
59
|
-
|
60
|
-
def calabash_android_version
|
61
|
-
`bundle exec calabash-android version`.strip
|
62
|
-
end
|
63
|
-
|
64
|
-
def is_ios?
|
65
|
-
app.end_with? ".ipa"
|
66
|
-
end
|
67
|
-
|
68
|
-
def test_server_path
|
69
|
-
require 'digest/md5'
|
70
|
-
digest = Digest::MD5.file(app).hexdigest
|
71
|
-
File.join("test_servers","#{digest}_#{calabash_android_version}.apk")
|
72
|
-
end
|
73
|
-
|
74
|
-
def all_files
|
75
|
-
dir = workspace
|
76
|
-
if features_zip
|
77
|
-
dir = Dir.mktmpdir
|
78
|
-
unzip_file(features_zip, dir)
|
79
|
-
dir = File.join(dir,File::Separator)
|
80
|
-
end
|
81
|
-
|
82
|
-
|
83
|
-
files = Dir.glob(File.join("#{dir}features","**","*"))
|
84
|
-
|
85
|
-
if File.directory?("#{dir}playback")
|
86
|
-
files += Dir.glob(File.join("#{dir}playback","*"))
|
87
|
-
end
|
88
|
-
|
89
|
-
|
90
|
-
files += Dir.glob(File.join("#{workspace}vendor","cache","*"))
|
91
|
-
|
92
|
-
if workspace and workspace.strip != ""
|
93
|
-
files += Dir.glob("#{workspace}Gemfile")
|
94
|
-
files += Dir.glob("#{workspace}Gemfile.lock")
|
95
|
-
end
|
96
|
-
|
97
|
-
if is_android?
|
98
|
-
files << test_server_path
|
99
|
-
end
|
100
|
-
p files
|
101
|
-
{:feature_prefix => dir, :workspace_prefix => workspace, :files => files.find_all { |file_or_dir| File.file? file_or_dir }}
|
102
|
-
end
|
103
|
-
|
104
|
-
def http_post(address, args, &block)
|
105
|
-
if block_given?
|
106
|
-
response = RestClient.post "#{host}/#{address}", args, {:content_type => "multipart/form-data"} do |response, request, result, &other_block|
|
107
|
-
block.call(response,request,result, &other_block)
|
108
|
-
end
|
109
|
-
else
|
110
|
-
response = RestClient.post "#{host}/#{address}", args
|
111
|
-
end
|
112
|
-
|
113
|
-
response.body
|
114
|
-
end
|
115
|
-
|
116
|
-
def is_windows?
|
117
|
-
(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
|
118
|
-
end
|
119
|
-
|
120
|
-
def is_macosx?
|
121
|
-
(RbConfig::CONFIG['host_os'] =~ /darwin/)
|
122
|
-
end
|
123
|
-
|
124
|
-
def validate_ipa(ipa)
|
125
|
-
result = false
|
126
|
-
dir = Dir.mktmpdir #do |dir|
|
127
|
-
|
128
|
-
unzip_file(ipa,dir)
|
129
|
-
unless File.directory?("#{dir}/Payload") #macos only
|
130
|
-
abort do
|
131
|
-
puts "Unzipping #{ipa} to #{dir} failed: Did not find a Payload directory (invalid .ipa)."
|
132
|
-
end
|
133
|
-
end
|
134
|
-
app_dir = Dir.foreach("#{dir}/Payload").find {|d| /\.app$/.match(d)}
|
135
|
-
app = app_dir.split(".")[0]
|
136
|
-
res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/#{app}" -o 2> /dev/null | grep CalabashServer`
|
137
|
-
|
138
|
-
if /CalabashServer/.match(res)
|
139
|
-
puts "ipa: #{ipa} *contains* calabash.framework"
|
140
|
-
result = :calabash
|
141
|
-
end
|
142
|
-
|
143
|
-
unless result
|
144
|
-
res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/#{app}" -o 2> /dev/null | grep FrankServer`
|
145
|
-
if /FrankServer/.match(res)
|
146
|
-
puts "ipa: #{ipa} *contains* FrankServer"
|
147
|
-
result = :frank
|
148
|
-
else
|
149
|
-
puts "ipa: #{ipa} *does not contain* calabash.framework"
|
150
|
-
result = false
|
151
|
-
end
|
152
|
-
end
|
153
|
-
#end
|
154
|
-
result
|
155
|
-
end
|
156
|
-
|
157
|
-
|
158
|
-
def self.log(message)
|
159
|
-
puts "#{Time.now } #{message}"
|
160
|
-
$stdout.flush
|
161
|
-
end
|
162
|
-
|
163
|
-
def self.log_header(message)
|
164
|
-
if is_windows?
|
165
|
-
puts "\n### #{message} ###"
|
166
|
-
else
|
167
|
-
puts "\n\e[#{35}m### #{message} ###\e[0m"
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
def usage
|
172
|
-
"Usage: lesspainful <app> <api_key> <opt workspace>? <opt feature.zip>?"
|
173
|
-
end
|
174
|
-
|
175
|
-
def verify_arguments
|
176
|
-
server = nil
|
177
|
-
log_and_abort(usage) unless app
|
178
|
-
log_and_abort(usage) unless api_key
|
179
|
-
abort_unless(File.exist?(app)) do
|
180
|
-
puts usage
|
181
|
-
puts "No such file: #{app}"
|
182
|
-
end
|
183
|
-
abort_unless(/\.(apk|ipa)$/ =~ app) do
|
184
|
-
puts usage
|
185
|
-
puts "<app> should be either an ipa or apk file"
|
186
|
-
end
|
187
|
-
if is_ios? and is_macosx? and not ENV['CHECK_IPA'] == '0'
|
188
|
-
log_header("Checking ipa for linking with Calabash or Frank")
|
189
|
-
server = validate_ipa(app)
|
190
|
-
abort_unless(server) do
|
191
|
-
puts "The .ipa file does not seem to be linked with Calabash."
|
192
|
-
puts "Verify that your app is linked correctly."
|
193
|
-
puts "To disable this check run with Environment Variable CHECK_IPA=0"
|
194
|
-
end
|
195
|
-
end
|
196
|
-
server
|
197
|
-
end
|
198
|
-
|
199
|
-
def abort(&block)
|
200
|
-
yield block
|
201
|
-
exit 1
|
202
|
-
end
|
203
|
-
|
204
|
-
def abort_unless(condition, &block)
|
205
|
-
unless condition
|
206
|
-
yield block
|
207
|
-
exit 1
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
def log_and_abort(message)
|
212
|
-
abort do
|
213
|
-
puts message
|
214
|
-
end
|
215
|
-
end
|
216
|
-
|
217
|
-
|
218
|
-
def self.verify_files
|
219
|
-
if is_android?
|
220
|
-
abort_unless(File.exist?(test_server_path)) do
|
221
|
-
puts "No test server found. Please run:"
|
222
|
-
puts " calabash-android build #{app}"
|
223
|
-
end
|
224
|
-
|
225
|
-
calabash_gem = Dir.glob("vendor/cache/calabash-android-*").first
|
226
|
-
abort_unless(calabash_gem) do
|
227
|
-
puts "calabash-android was not packaged correct."
|
228
|
-
puts "Please tell contact@lesspainful.com about this bug."
|
229
|
-
end
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
2
|
|
3
|
+
require 'lesspainful/cli'
|
234
4
|
|
5
|
+
begin
|
235
6
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
log("Creating Gemfile for Android")
|
245
|
-
tgt = File.join(File.dirname(__FILE__),"..","lib","GemfileAndroid")
|
246
|
-
elsif is_ios?
|
247
|
-
log("Creating Gemfile for iOS")
|
248
|
-
gemfile = "GemfileIOS"
|
249
|
-
if server == :frank
|
250
|
-
gemfile = "GemfileIOSFrank"
|
251
|
-
end
|
252
|
-
tgt = File.join(File.dirname(__FILE__),"..","lib",gemfile)
|
253
|
-
else
|
254
|
-
abort do
|
255
|
-
puts usage
|
256
|
-
puts "Your app (second argument) must be an ipa or apk file."
|
257
|
-
end
|
258
|
-
|
259
|
-
end
|
260
|
-
FileUtils.cp(File.expand_path(tgt), "Gemfile")
|
261
|
-
end
|
262
|
-
|
263
|
-
log_header("Packaging")
|
264
|
-
log_and_abort "Bundler failed. Please check command: bundle package" unless system("bundle package --all")
|
265
|
-
|
266
|
-
|
267
|
-
log_header("Collecting files")
|
268
|
-
collected_files = all_files
|
269
|
-
file_paths = collected_files[:files]
|
270
|
-
feature_prefix = collected_files[:feature_prefix]
|
271
|
-
workspace_prefix = collected_files[:workspace_prefix]
|
272
|
-
|
273
|
-
hashes = file_paths.collect { |f| digest(f) }
|
274
|
-
hashes << digest(app)
|
275
|
-
|
276
|
-
log_header("Verifying files")
|
277
|
-
|
278
|
-
verify_files
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
log_header("Negotiating upload")
|
284
|
-
|
285
|
-
response = http_post("check_hash", {"hashes" => hashes})
|
286
|
-
|
287
|
-
cache_status = JSON.parse(response)
|
288
|
-
|
289
|
-
curl_args = []
|
290
|
-
files = []
|
291
|
-
paths = []
|
292
|
-
file_paths.each do |file|
|
293
|
-
if cache_status[digest(file)]
|
294
|
-
#Server already knows about this file. No need to upload it.
|
295
|
-
files << digest(file)
|
7
|
+
script = LessPainful::CLI.new
|
8
|
+
if ARGV.count == 0
|
9
|
+
script.invoke(:help)
|
10
|
+
elsif LessPainful::CLI.all_tasks.keys.include?(ARGV[0])
|
11
|
+
LessPainful::CLI.start
|
12
|
+
elsif ARGV.count >= 2
|
13
|
+
#For backwards compat allow "old-style"
|
14
|
+
script.invoke(:submit, ARGV)
|
296
15
|
else
|
297
|
-
|
298
|
-
files << File.new(file)
|
16
|
+
script.invoke(:help)
|
299
17
|
end
|
300
18
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
19
|
+
rescue Exception=>e
|
20
|
+
if ENV['DEBUG']
|
21
|
+
puts e.backtrace.join("\n") if e.backtrace
|
22
|
+
puts "Message:"
|
305
23
|
end
|
306
|
-
|
24
|
+
puts e.message
|
25
|
+
exit(false)
|
307
26
|
end
|
308
|
-
|
309
|
-
app_file = cache_status[digest(app)] ? digest(app) : File.new(app)
|
310
|
-
|
311
|
-
log_header("Uploading negotiated files")
|
312
|
-
response = http_post("upload", {"files" => files,
|
313
|
-
"paths" => paths,
|
314
|
-
"app" => app_file,
|
315
|
-
"api_key" => api_key,
|
316
|
-
"app_filename" => File.basename(app)}) do |response, request, result, &block|
|
317
|
-
case response.code
|
318
|
-
when 200
|
319
|
-
response
|
320
|
-
when 403
|
321
|
-
abort do
|
322
|
-
puts "Invalid API key"
|
323
|
-
end
|
324
|
-
when 413
|
325
|
-
abort do
|
326
|
-
puts "Files too large"
|
327
|
-
end
|
328
|
-
else
|
329
|
-
abort do
|
330
|
-
log "Unexpected Error. Please contact contact@lesspainful.com and provide the timestamp on your left."
|
331
|
-
end
|
332
|
-
end
|
333
|
-
end
|
334
|
-
|
335
|
-
end_at = Time.now
|
336
|
-
|
337
|
-
log_header("Done (took %.1fs)" % (end_at - start_at))
|
338
|
-
log response
|
339
|
-
|
data/lesspainful.gemspec
CHANGED
@@ -12,11 +12,13 @@ Gem::Specification.new do |s|
|
|
12
12
|
s.summary = %q{Client for uploading calabash test to www.lesspainful.com}
|
13
13
|
s.description = %q{www.lesspainful.com lets you run calabash tests for native and hybrid apps on physical devices}
|
14
14
|
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = Dir.glob("test/**/*.rb")
|
15
16
|
s.executables = "lesspainful"
|
16
17
|
s.require_paths = ["lib"]
|
17
18
|
|
19
|
+
s.add_dependency( "thor", "0.17.0")
|
18
20
|
s.add_dependency( "bundler","~> 1.2")
|
19
21
|
s.add_dependency( "json")
|
20
|
-
s.add_dependency( "rubyzip")
|
21
|
-
s.add_dependency( "rest-client")
|
22
|
+
s.add_dependency( "rubyzip","0.9.9")
|
23
|
+
s.add_dependency( "rest-client","1.6.7")
|
22
24
|
end
|
@@ -0,0 +1,547 @@
|
|
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 LessPainful
|
13
|
+
class CLI < Thor
|
14
|
+
include Thor::Actions
|
15
|
+
|
16
|
+
attr_accessor :host, :app, :api_key, :workspace, :config, :profile, :features_zip, :skip_check, :reset_between_scenarios, :dry_run
|
17
|
+
|
18
|
+
attr_accessor :endpoint_path
|
19
|
+
|
20
|
+
def self.source_root
|
21
|
+
File.join(File.dirname(__FILE__), '..')
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "submit <APP> <API_KEY>", "Submits your app and test suite to LessPainful's device labs"
|
25
|
+
|
26
|
+
method_option :host,
|
27
|
+
:desc => "Device Lab Host to submit to.",
|
28
|
+
:aliases => '-h', :type => :string,
|
29
|
+
:default => (ENV["LP_HOST"] || 'https://www.lesspainful.com')
|
30
|
+
|
31
|
+
method_option :workspace,
|
32
|
+
:desc => "Workspace containing Gemfile and features.",
|
33
|
+
:aliases => '-w',
|
34
|
+
:type => :string,
|
35
|
+
:default => File.expand_path(".")
|
36
|
+
|
37
|
+
method_option :features,
|
38
|
+
:desc => "Zip file with features, step definitions, etc.",
|
39
|
+
:aliases => '-f',
|
40
|
+
:type => :string
|
41
|
+
|
42
|
+
method_option :config,
|
43
|
+
:desc => "Cucumber configuration file (cucumber.yml).",
|
44
|
+
:aliases => '-c',
|
45
|
+
:type => :string
|
46
|
+
|
47
|
+
method_option :profile,
|
48
|
+
:desc => "Profile to run (profile from cucumber.yml).",
|
49
|
+
:aliases => '-p',
|
50
|
+
:type => :string
|
51
|
+
|
52
|
+
method_option "skip-check",
|
53
|
+
:desc => "Skip checking for ipa linked with Calabash (iOS only).",
|
54
|
+
:type => :boolean
|
55
|
+
|
56
|
+
method_option "reset-between-scenarios",
|
57
|
+
:desc => "Reinstall app between each scenario (iOS only).",
|
58
|
+
:type => :string,
|
59
|
+
:default => ENV['RESET_BETWEEN_SCENARIOS']=='1' ? '1': "0"
|
60
|
+
|
61
|
+
method_option "dry-run",
|
62
|
+
:desc => "Sanity check only, don't upload.",
|
63
|
+
:aliases => '-d',
|
64
|
+
:type => :boolean,
|
65
|
+
:default => false #do upload by default
|
66
|
+
|
67
|
+
def submit(app, api_key, *args)
|
68
|
+
|
69
|
+
self.host = options[:host]
|
70
|
+
|
71
|
+
unless self.host
|
72
|
+
self.endpoint_path = "upload" #invoked old-style
|
73
|
+
self.host = ENV["LP_HOST"] || 'https://www.lesspainful.com'
|
74
|
+
else
|
75
|
+
self.endpoint_path = "upload2" #nginx receives upload
|
76
|
+
end
|
77
|
+
|
78
|
+
app_path = File.expand_path(app)
|
79
|
+
unless File.exist?(app_path)
|
80
|
+
raise "App is not a file: #{app_path}"
|
81
|
+
end
|
82
|
+
|
83
|
+
self.app = app_path
|
84
|
+
|
85
|
+
self.dry_run = options["dry-run"]
|
86
|
+
|
87
|
+
self.api_key = api_key
|
88
|
+
|
89
|
+
self.skip_check = ENV['CHECK_IPA'] == '0'
|
90
|
+
self.skip_check = options["skip-check"] unless options["skip-check"].nil?
|
91
|
+
|
92
|
+
self.reset_between_scenarios = options["reset-between-scenarios"]
|
93
|
+
|
94
|
+
parse_and_set_config_and_profile
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
workspace_path = options[:workspace] || File.expand_path(".")
|
99
|
+
|
100
|
+
if args[0]
|
101
|
+
workspace_path = args[0]
|
102
|
+
end
|
103
|
+
|
104
|
+
unless File.directory?(workspace_path)
|
105
|
+
raise "Provided workspace: #{workspace_path} is not a directory."
|
106
|
+
end
|
107
|
+
self.workspace = File.join(File.expand_path(workspace_path), File::Separator)
|
108
|
+
|
109
|
+
|
110
|
+
features_path = options[:features]
|
111
|
+
features_path = args[1] if args[1]
|
112
|
+
|
113
|
+
unless features_path.nil?
|
114
|
+
if File.exist?(features_path)
|
115
|
+
self.features_zip = File.expand_path(features_path)
|
116
|
+
else
|
117
|
+
raise "Provided features file does not exist #{features_path}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
if ENV['DEBUG']
|
122
|
+
puts "Host = #{self.host}"
|
123
|
+
puts "App = #{self.app}"
|
124
|
+
puts "API Key = #{self.api_key}"
|
125
|
+
puts "Workspace = #{self.workspace}"
|
126
|
+
puts "Features Zip = #{self.features_zip}"
|
127
|
+
puts "Config = #{self.config}"
|
128
|
+
puts "Profile = #{self.profile}"
|
129
|
+
puts "Skip Check = #{self.skip_check}"
|
130
|
+
puts "Reset Between Scenarios = #{self.reset_between_scenarios}"
|
131
|
+
end
|
132
|
+
|
133
|
+
#Argument parsing done
|
134
|
+
json = submit_test_job
|
135
|
+
|
136
|
+
unless dry_run
|
137
|
+
if JSON.respond_to?(:pretty_generate)
|
138
|
+
puts JSON.pretty_generate(json)
|
139
|
+
else
|
140
|
+
puts json
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
default_task :submit
|
147
|
+
|
148
|
+
no_tasks do
|
149
|
+
|
150
|
+
def submit_test_job
|
151
|
+
start_at = Time.now
|
152
|
+
|
153
|
+
server = verify_app_and_extract_test_server
|
154
|
+
|
155
|
+
|
156
|
+
log_header("Checking for Gemfile")
|
157
|
+
gemfile_path = File.join(self.workspace, "Gemfile")
|
158
|
+
unless File.exist?(gemfile_path)
|
159
|
+
copy_default_gemfile(gemfile_path, server)
|
160
|
+
end
|
161
|
+
|
162
|
+
log_header("Packaging")
|
163
|
+
FileUtils.cd(self.workspace) do
|
164
|
+
unless system("bundle package --all")
|
165
|
+
log_and_abort "Bundler failed. Please check command: bundle package"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
log_header("Verifying dependencies")
|
171
|
+
verify_dependencies
|
172
|
+
|
173
|
+
|
174
|
+
if dry_run
|
175
|
+
log_header("Dry run only")
|
176
|
+
log("Dry run completed OK")
|
177
|
+
return
|
178
|
+
end
|
179
|
+
|
180
|
+
app_file, files, paths = gather_files_and_paths_to_upload(all_files)
|
181
|
+
|
182
|
+
|
183
|
+
log_header("Uploading negotiated files")
|
184
|
+
|
185
|
+
upload_data = {"files" => files,
|
186
|
+
"paths" => paths,
|
187
|
+
"reset_between_scenarios" => reset_between_scenarios,
|
188
|
+
"app" => app_file,
|
189
|
+
"api_key" => api_key,
|
190
|
+
"app_filename" => File.basename(app)}
|
191
|
+
|
192
|
+
if profile #only if config and profile
|
193
|
+
upload_data["profile"] = profile
|
194
|
+
end
|
195
|
+
|
196
|
+
if ENV['DEBUG']
|
197
|
+
puts JSON.pretty_generate(upload_data)
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
response = http_post(endpoint_path,upload_data) do |response, request, result, &block|
|
202
|
+
case response.code
|
203
|
+
when 200..202
|
204
|
+
response
|
205
|
+
when 403
|
206
|
+
abort do
|
207
|
+
puts "Invalid API key"
|
208
|
+
end
|
209
|
+
when 413
|
210
|
+
abort do
|
211
|
+
puts "Files too large"
|
212
|
+
end
|
213
|
+
else
|
214
|
+
abort do
|
215
|
+
log "Unexpected Error. Please contact contact@lesspainful.com and provide the timestamp on your left."
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
end_at = Time.now
|
221
|
+
|
222
|
+
log_header("Done (took %.1fs)" % (end_at - start_at))
|
223
|
+
|
224
|
+
return :status => response.code, :body => JSON.parse(response)
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
def copy_default_gemfile(gemfile_path, server)
|
229
|
+
log("")
|
230
|
+
log("Gemfile missing.")
|
231
|
+
log("You must provide a Gemfile in your workspace.")
|
232
|
+
log("A Gemfile must describe your dependencies and their versions")
|
233
|
+
log("See: http://gembundler.com/v1.2/gemfile.html")
|
234
|
+
log("")
|
235
|
+
log("Warning proceeding with default Gemfile.")
|
236
|
+
log("It is strongly recommended that you create a custom Gemfile.")
|
237
|
+
tgt = nil
|
238
|
+
if is_android?
|
239
|
+
log("Creating Gemfile for Android")
|
240
|
+
tgt = File.join(CLI.source_root, "GemfileAndroid")
|
241
|
+
elsif is_ios?
|
242
|
+
log("Creating Gemfile for iOS")
|
243
|
+
gemfile = "GemfileIOS"
|
244
|
+
if server == :frank
|
245
|
+
gemfile = "GemfileIOSFrank"
|
246
|
+
end
|
247
|
+
tgt = File.join(CLI.source_root, gemfile)
|
248
|
+
else
|
249
|
+
raise "Your app must be an ipa or apk file."
|
250
|
+
end
|
251
|
+
log("Proceeding with Gemfile: #{gemfile_path}")
|
252
|
+
|
253
|
+
|
254
|
+
FileUtils.cp(File.expand_path(tgt), gemfile_path)
|
255
|
+
|
256
|
+
puts(File.read(gemfile_path))
|
257
|
+
|
258
|
+
log("")
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
def gather_files_and_paths_to_upload(collected_files)
|
263
|
+
|
264
|
+
log_header("Calculating digests")
|
265
|
+
|
266
|
+
file_paths = collected_files[:files]
|
267
|
+
feature_prefix = collected_files[:feature_prefix]
|
268
|
+
workspace_prefix = collected_files[:workspace_prefix]
|
269
|
+
|
270
|
+
hashes = file_paths.collect { |f| digest(f) }
|
271
|
+
hashes << digest(app)
|
272
|
+
|
273
|
+
|
274
|
+
log_header("Negotiating upload")
|
275
|
+
|
276
|
+
response = http_post("check_hash", {"hashes" => hashes})
|
277
|
+
|
278
|
+
cache_status = JSON.parse(response)
|
279
|
+
|
280
|
+
log_header("Gathering files based on negotiation")
|
281
|
+
|
282
|
+
files = []
|
283
|
+
paths = []
|
284
|
+
file_paths.each do |file|
|
285
|
+
if cache_status[digest(file)]
|
286
|
+
#Server already knows about this file. No need to upload it.
|
287
|
+
files << digest(file)
|
288
|
+
else
|
289
|
+
#Upload file
|
290
|
+
files << File.new(file)
|
291
|
+
end
|
292
|
+
|
293
|
+
if file.start_with?(feature_prefix)
|
294
|
+
prefix = feature_prefix
|
295
|
+
else
|
296
|
+
prefix = workspace_prefix
|
297
|
+
end
|
298
|
+
paths << file.sub(prefix, "")
|
299
|
+
end
|
300
|
+
|
301
|
+
if config
|
302
|
+
files << File.new(config)
|
303
|
+
paths << "config/cucumber.yml"
|
304
|
+
end
|
305
|
+
|
306
|
+
app_file = cache_status[digest(app)] ? digest(app) : File.new(app)
|
307
|
+
|
308
|
+
return app_file, files, paths
|
309
|
+
end
|
310
|
+
|
311
|
+
def digest(file)
|
312
|
+
Digest::SHA256.file(file).hexdigest
|
313
|
+
end
|
314
|
+
|
315
|
+
def unzip_file (file, destination)
|
316
|
+
Zip::ZipFile.open(file) { |zip_file|
|
317
|
+
zip_file.each { |f|
|
318
|
+
f_path=File.join(destination, f.name)
|
319
|
+
FileUtils.mkdir_p(File.dirname(f_path))
|
320
|
+
zip_file.extract(f, f_path) unless File.exist?(f_path)
|
321
|
+
}
|
322
|
+
}
|
323
|
+
end
|
324
|
+
|
325
|
+
|
326
|
+
def is_android?
|
327
|
+
app.end_with? ".apk"
|
328
|
+
end
|
329
|
+
|
330
|
+
def calabash_android_version
|
331
|
+
`bundle exec calabash-android version`.strip
|
332
|
+
end
|
333
|
+
|
334
|
+
def is_ios?
|
335
|
+
app.end_with? ".ipa"
|
336
|
+
end
|
337
|
+
|
338
|
+
def test_server_path
|
339
|
+
require 'digest/md5'
|
340
|
+
digest = Digest::MD5.file(app).hexdigest
|
341
|
+
File.join("test_servers", "#{digest}_#{calabash_android_version}.apk")
|
342
|
+
end
|
343
|
+
|
344
|
+
def all_files
|
345
|
+
dir = workspace
|
346
|
+
if features_zip
|
347
|
+
dir = Dir.mktmpdir
|
348
|
+
unzip_file(features_zip, dir)
|
349
|
+
dir = File.join(dir, File::Separator)
|
350
|
+
end
|
351
|
+
|
352
|
+
|
353
|
+
files = Dir.glob(File.join("#{dir}features", "**", "*"))
|
354
|
+
|
355
|
+
if File.directory?("#{dir}playback")
|
356
|
+
files += Dir.glob(File.join("#{dir}playback", "*"))
|
357
|
+
end
|
358
|
+
|
359
|
+
if config
|
360
|
+
files << config
|
361
|
+
end
|
362
|
+
|
363
|
+
|
364
|
+
files += Dir.glob(File.join("#{workspace}vendor", "cache", "*"))
|
365
|
+
|
366
|
+
if workspace and workspace.strip != ""
|
367
|
+
files += Dir.glob("#{workspace}Gemfile")
|
368
|
+
files += Dir.glob("#{workspace}Gemfile.lock")
|
369
|
+
end
|
370
|
+
|
371
|
+
if is_android?
|
372
|
+
files << test_server_path
|
373
|
+
end
|
374
|
+
p files
|
375
|
+
{:feature_prefix => dir, :workspace_prefix => workspace, :files => files.find_all { |file_or_dir| File.file? file_or_dir }}
|
376
|
+
end
|
377
|
+
|
378
|
+
def http_post(address, args, &block)
|
379
|
+
if block_given?
|
380
|
+
response = RestClient.post "#{host}/#{address}", args, {:content_type => "multipart/form-data"} do |response, request, result, &other_block|
|
381
|
+
block.call(response, request, result, &other_block)
|
382
|
+
end
|
383
|
+
else
|
384
|
+
response = RestClient.post "#{host}/#{address}", args
|
385
|
+
end
|
386
|
+
|
387
|
+
response.body
|
388
|
+
end
|
389
|
+
|
390
|
+
def is_windows?
|
391
|
+
(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
|
392
|
+
end
|
393
|
+
|
394
|
+
def is_macosx?
|
395
|
+
(RbConfig::CONFIG['host_os'] =~ /darwin/)
|
396
|
+
end
|
397
|
+
|
398
|
+
def validate_ipa(ipa)
|
399
|
+
result = false
|
400
|
+
dir = Dir.mktmpdir #do |dir|
|
401
|
+
|
402
|
+
unzip_file(ipa, dir)
|
403
|
+
unless File.directory?("#{dir}/Payload") #macos only
|
404
|
+
raise "Unzipping #{ipa} to #{dir} failed: Did not find a Payload directory (invalid .ipa)."
|
405
|
+
end
|
406
|
+
app_dir = Dir.foreach("#{dir}/Payload").find { |d| /\.app$/.match(d) }
|
407
|
+
app = app_dir.split(".")[0]
|
408
|
+
res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/#{app}" -o 2> /dev/null | grep CalabashServer`
|
409
|
+
|
410
|
+
if /CalabashServer/.match(res)
|
411
|
+
puts "ipa: #{ipa} *contains* calabash.framework"
|
412
|
+
result = :calabash
|
413
|
+
end
|
414
|
+
|
415
|
+
unless result
|
416
|
+
res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/#{app}" -o 2> /dev/null | grep FrankServer`
|
417
|
+
if /FrankServer/.match(res)
|
418
|
+
puts "ipa: #{ipa} *contains* FrankServer"
|
419
|
+
result = :frank
|
420
|
+
else
|
421
|
+
puts "ipa: #{ipa} *does not contain* calabash.framework"
|
422
|
+
result = false
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
result
|
427
|
+
end
|
428
|
+
|
429
|
+
|
430
|
+
def log(message)
|
431
|
+
puts "#{Time.now } #{message}"
|
432
|
+
$stdout.flush
|
433
|
+
end
|
434
|
+
|
435
|
+
def log_header(message)
|
436
|
+
if is_windows?
|
437
|
+
puts "\n### #{message} ###"
|
438
|
+
else
|
439
|
+
puts "\n\e[#{35}m### #{message} ###\e[0m"
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
|
444
|
+
def verify_app_and_extract_test_server
|
445
|
+
server = nil
|
446
|
+
|
447
|
+
unless File.exist?(app)
|
448
|
+
raise "No such file: #{app}"
|
449
|
+
end
|
450
|
+
unless (/\.(apk|ipa)$/ =~ app)
|
451
|
+
raise "<APP> should be either an ipa or apk file."
|
452
|
+
end
|
453
|
+
if is_ios? and is_macosx? and not skip_check
|
454
|
+
log_header("Checking ipa for linking with Calabash or Frank")
|
455
|
+
server = validate_ipa(app)
|
456
|
+
abort_unless(server) do
|
457
|
+
puts "The .ipa file does not seem to be linked with Calabash."
|
458
|
+
puts "Verify that your app is linked correctly."
|
459
|
+
puts "To disable this check run with --skip-check or set Environment Variable CHECK_IPA=0"
|
460
|
+
end
|
461
|
+
end
|
462
|
+
server
|
463
|
+
end
|
464
|
+
|
465
|
+
def abort(&block)
|
466
|
+
yield block
|
467
|
+
exit 1
|
468
|
+
end
|
469
|
+
|
470
|
+
def abort_unless(condition, &block)
|
471
|
+
unless condition
|
472
|
+
yield block
|
473
|
+
exit 1
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
def log_and_abort(message)
|
478
|
+
abort do
|
479
|
+
puts message
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
|
484
|
+
def verify_dependencies
|
485
|
+
if is_android?
|
486
|
+
abort_unless(File.exist?(test_server_path)) do
|
487
|
+
puts "No test server found. Please run:"
|
488
|
+
puts " calabash-android build #{app}"
|
489
|
+
end
|
490
|
+
|
491
|
+
calabash_gem = Dir.glob("vendor/cache/calabash-android-*").first
|
492
|
+
abort_unless(calabash_gem) do
|
493
|
+
puts "calabash-android was not packaged correct."
|
494
|
+
puts "Please tell contact@lesspainful.com about this bug."
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
|
500
|
+
def parse_and_set_config_and_profile
|
501
|
+
config_path = options[:config]
|
502
|
+
if config_path
|
503
|
+
config_path = File.expand_path(config_path)
|
504
|
+
unless File.exist?(config_path)
|
505
|
+
raise "Config file does not exist #{config_path}"
|
506
|
+
end
|
507
|
+
|
508
|
+
|
509
|
+
begin
|
510
|
+
config_yml = YAML.load_file(config_path)
|
511
|
+
rescue Exception => e
|
512
|
+
puts "Unable to parse #{config_path} as yml. Is this your Cucumber.yml file?"
|
513
|
+
raise e
|
514
|
+
end
|
515
|
+
|
516
|
+
if ENV['DEBUG']
|
517
|
+
puts "Parsed Cucumber config as:"
|
518
|
+
puts config_yml.inspect
|
519
|
+
end
|
520
|
+
|
521
|
+
profile = options[:profile]
|
522
|
+
unless profile
|
523
|
+
raise "Config file provided but no profile selected."
|
524
|
+
else
|
525
|
+
unless config_yml[profile]
|
526
|
+
raise "Config file provided did not contain profile #{profile}."
|
527
|
+
else
|
528
|
+
puts "Using profile #{profile}..."
|
529
|
+
self.profile = profile
|
530
|
+
end
|
531
|
+
end
|
532
|
+
else
|
533
|
+
if options[:profile]
|
534
|
+
raise "Profile selected but no config file provided."
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
self.config = config_path
|
539
|
+
end
|
540
|
+
|
541
|
+
end
|
542
|
+
|
543
|
+
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
|
data/lib/version.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
a: 42
|
data/test/test_parser.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'thor'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'lesspainful/cli'
|
5
|
+
|
6
|
+
class TestParser < Test::Unit::TestCase
|
7
|
+
def test_should_raise_if_no_app_or_api_key_is_given
|
8
|
+
script = LessPainful::CLI.new([])
|
9
|
+
assert_raise Thor::InvocationError do
|
10
|
+
script.invoke(:submit)
|
11
|
+
end
|
12
|
+
|
13
|
+
script = LessPainful::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 = LessPainful::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 = LessPainful::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 = LessPainful::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
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lesspainful
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,24 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-03-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thor
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - '='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.17.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - '='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.17.0
|
14
30
|
- !ruby/object:Gem::Dependency
|
15
31
|
name: bundler
|
16
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -48,33 +64,33 @@ dependencies:
|
|
48
64
|
requirement: !ruby/object:Gem::Requirement
|
49
65
|
none: false
|
50
66
|
requirements:
|
51
|
-
- -
|
67
|
+
- - '='
|
52
68
|
- !ruby/object:Gem::Version
|
53
|
-
version:
|
69
|
+
version: 0.9.9
|
54
70
|
type: :runtime
|
55
71
|
prerelease: false
|
56
72
|
version_requirements: !ruby/object:Gem::Requirement
|
57
73
|
none: false
|
58
74
|
requirements:
|
59
|
-
- -
|
75
|
+
- - '='
|
60
76
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
77
|
+
version: 0.9.9
|
62
78
|
- !ruby/object:Gem::Dependency
|
63
79
|
name: rest-client
|
64
80
|
requirement: !ruby/object:Gem::Requirement
|
65
81
|
none: false
|
66
82
|
requirements:
|
67
|
-
- -
|
83
|
+
- - '='
|
68
84
|
- !ruby/object:Gem::Version
|
69
|
-
version:
|
85
|
+
version: 1.6.7
|
70
86
|
type: :runtime
|
71
87
|
prerelease: false
|
72
88
|
version_requirements: !ruby/object:Gem::Requirement
|
73
89
|
none: false
|
74
90
|
requirements:
|
75
|
-
- -
|
91
|
+
- - '='
|
76
92
|
- !ruby/object:Gem::Version
|
77
|
-
version:
|
93
|
+
version: 1.6.7
|
78
94
|
description: www.lesspainful.com lets you run calabash tests for native and hybrid
|
79
95
|
apps on physical devices
|
80
96
|
email:
|
@@ -93,7 +109,9 @@ files:
|
|
93
109
|
- lib/GemfileAndroid
|
94
110
|
- lib/GemfileIOS
|
95
111
|
- lib/GemfileIOSFrank
|
112
|
+
- lib/lesspainful/cli.rb
|
96
113
|
- lib/version.rb
|
114
|
+
- test/config/cucumber.yml
|
97
115
|
- test/ipa/.irbrc
|
98
116
|
- test/ipa/2012 Olympics_cal.ipa
|
99
117
|
- test/ipa/2012 Olympics_no_cal.ipa
|
@@ -108,6 +126,7 @@ files:
|
|
108
126
|
- test/ipa/features/support/launch.rb
|
109
127
|
- test/ipa/irb_ios4.sh
|
110
128
|
- test/ipa/irb_ios5.sh
|
129
|
+
- test/test_parser.rb
|
111
130
|
homepage: http://www.lesspainful.com
|
112
131
|
licenses: []
|
113
132
|
post_install_message:
|
@@ -132,5 +151,11 @@ rubygems_version: 1.8.23
|
|
132
151
|
signing_key:
|
133
152
|
specification_version: 3
|
134
153
|
summary: Client for uploading calabash test to www.lesspainful.com
|
135
|
-
test_files:
|
154
|
+
test_files:
|
155
|
+
- test/ipa/features/step_definitions/calabash_steps.rb
|
156
|
+
- test/ipa/features/step_definitions/my_first_steps.rb
|
157
|
+
- test/ipa/features/support/env.rb
|
158
|
+
- test/ipa/features/support/hooks.rb
|
159
|
+
- test/ipa/features/support/launch.rb
|
160
|
+
- test/test_parser.rb
|
136
161
|
has_rdoc:
|