smart_monkey 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +17 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +77 -0
  6. data/Rakefile +57 -0
  7. data/Troubleshooting.md +61 -0
  8. data/VERSION +1 -0
  9. data/bin/smart_monkey +53 -0
  10. data/lib/bootstrap/css/bootstrap-responsive.css +1109 -0
  11. data/lib/bootstrap/css/bootstrap-responsive.min.css +9 -0
  12. data/lib/bootstrap/css/bootstrap.css +6167 -0
  13. data/lib/bootstrap/css/bootstrap.min.css +9 -0
  14. data/lib/bootstrap/img/glyphicons-halflings-white.png +0 -0
  15. data/lib/bootstrap/img/glyphicons-halflings.png +0 -0
  16. data/lib/bootstrap/js/bootstrap.js +2280 -0
  17. data/lib/bootstrap/js/bootstrap.min.js +6 -0
  18. data/lib/ios_device_log/deviceconsole +0 -0
  19. data/lib/smart_monkey.rb +2 -0
  20. data/lib/smart_monkey/command_helper.rb +71 -0
  21. data/lib/smart_monkey/monkey_runner.rb +549 -0
  22. data/lib/smart_monkey/templates/automation_result.xsl +61 -0
  23. data/lib/smart_monkey/templates/index.html.erb +77 -0
  24. data/lib/smart_monkey/templates/result.html.erb +110 -0
  25. data/lib/smart_monkey/templates/result_view.coffee +160 -0
  26. data/lib/smart_monkey/templates/result_view.js +250 -0
  27. data/lib/ui-auto-monkey/UIAutoMonkey.js +470 -0
  28. data/lib/ui-auto-monkey/custom.js +73 -0
  29. data/lib/ui-auto-monkey/handler/buttonHandler.js +111 -0
  30. data/lib/ui-auto-monkey/handler/wbScrollViewButtonHandler.js +114 -0
  31. data/lib/ui-auto-monkey/tuneup/LICENSE +20 -0
  32. data/lib/ui-auto-monkey/tuneup/assertions.js +402 -0
  33. data/lib/ui-auto-monkey/tuneup/image_asserter +26 -0
  34. data/lib/ui-auto-monkey/tuneup/image_assertion.js +65 -0
  35. data/lib/ui-auto-monkey/tuneup/image_assertion.rb +102 -0
  36. data/lib/ui-auto-monkey/tuneup/lang-ext.js +76 -0
  37. data/lib/ui-auto-monkey/tuneup/screen.js +11 -0
  38. data/lib/ui-auto-monkey/tuneup/test.js +71 -0
  39. data/lib/ui-auto-monkey/tuneup/test_runner/abbreviated_console_output.rb +38 -0
  40. data/lib/ui-auto-monkey/tuneup/test_runner/colored_console_output.rb +27 -0
  41. data/lib/ui-auto-monkey/tuneup/test_runner/console_output.rb +17 -0
  42. data/lib/ui-auto-monkey/tuneup/test_runner/preprocessor.rb +25 -0
  43. data/lib/ui-auto-monkey/tuneup/test_runner/run +343 -0
  44. data/lib/ui-auto-monkey/tuneup/test_runner/xunit_output.rb +114 -0
  45. data/lib/ui-auto-monkey/tuneup/tuneup.js +6 -0
  46. data/lib/ui-auto-monkey/tuneup/tuneup_js.podspec +52 -0
  47. data/lib/ui-auto-monkey/tuneup/uiautomation-ext.js +965 -0
  48. data/smart_monkey.gemspec +112 -0
  49. data/spec/spec_helper.rb +12 -0
  50. metadata +192 -0
@@ -0,0 +1,25 @@
1
+ class Preprocessor
2
+ def self.process(file)
3
+ self.process_imports(file, [])
4
+ end
5
+
6
+ private
7
+ IMPORT_STATEMENT = /#import "([^"]+)"/
8
+ def self.process_imports(file, imported_file_names)
9
+ content = File.read(file)
10
+ content.gsub(IMPORT_STATEMENT) do
11
+ next if imported_file_names.include? $1
12
+
13
+ imported_file_names << $1
14
+ import_file = File.join(File.dirname(file), $1)
15
+ begin
16
+ "// begin #{File.basename($1)}" << "\n" <<
17
+ process_imports(import_file, imported_file_names) << "\n" <<
18
+ "// end #{File.basename($1)}" << "\n"
19
+ rescue Exception => e
20
+ STDERR.puts "Unable to process file #{import_file}: #{e}"
21
+ $&
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env ruby -wKU
2
+
3
+ require "fileutils"
4
+ require "optparse"
5
+ require "ostruct"
6
+ require "pty"
7
+ require "tempfile"
8
+ require File.expand_path(File.join(File.dirname(__FILE__), "preprocessor"))
9
+ require File.expand_path(File.join(File.dirname(__FILE__), "console_output"))
10
+ require File.expand_path(File.join(File.dirname(__FILE__), "colored_console_output"))
11
+ require File.expand_path(File.join(File.dirname(__FILE__), "abbreviated_console_output"))
12
+ require File.expand_path(File.join(File.dirname(__FILE__), "xunit_output"))
13
+
14
+ options = OpenStruct.new
15
+ options.env_vars = {}
16
+ options.attempts = 1
17
+ options.startup_timeout = 30
18
+
19
+ opts_parser = OptionParser.new do |opts|
20
+ opts.banner = "USAGE: run-test-script <app bundle> <test script> <output directory> [optional args]" +
21
+ "\n If <app bundle> is the name of the app without extension, the newest bundle is located autmatically"
22
+
23
+ opts.on("-d", "--device DEVICE", "Device UDID to run test against,",
24
+ "or 'dynamic' to find the UDID at runtime") do |dev|
25
+ options.device = dev
26
+ end
27
+
28
+ opts.on("-w", "--simName SIMULATORNAME", "Simulator name to target") do |dev|
29
+ options.simulatorName = dev
30
+ end
31
+
32
+ opts.on("-e", "--environment ENV", "Pass variables in the form of name=value") do |env|
33
+ key, value = env.split '='
34
+ options.env_vars[key] = value
35
+ end
36
+
37
+ opts.on_tail("-h", "--help", "Show this message") do
38
+ puts opts
39
+ exit 0
40
+ end
41
+
42
+ opts.on("-t", "--timeout DURATION", OptionParser::DecimalInteger,
43
+ "Maximum time in seconds with no output") do |t|
44
+ options.timeout = t
45
+ end
46
+
47
+ opts.on("-r", "--run TEST", "Only run tests named alike. It's possible to use RegExps.") do |test|
48
+ options.run = test
49
+ end
50
+
51
+ opts.on("-v", "--verbose", "Produce more output") do
52
+ options.verbose = true
53
+ end
54
+
55
+ opts.on("-q", "--quiet", "Don't print test results to console") do
56
+ options.quiet = true
57
+ end
58
+
59
+ opts.on("-c", "--color", "Colorize the console output") do
60
+ options.colorize = true
61
+ end
62
+
63
+ opts.on("-b", "--brief", "Abbreviated and colorized console output") do
64
+ options.brief = true
65
+ end
66
+
67
+ opts.on("-p", "--preprocess", "Use own preprocessor to substitude imports") do
68
+ options.preprocess = true
69
+ end
70
+
71
+ opts.on("-x", "--xunit", "Create Xunit formatted test result file in the output directory") do
72
+ options.xunit = true;
73
+ end
74
+
75
+ opts.on("-l", "--language=en", "Use language to simulator") do |l|
76
+ options.language = "-AppleLanguages \(#{l}\)"
77
+ end
78
+
79
+ opts.on("-a", "--attempts ATTEMPTS", OptionParser::DecimalInteger,
80
+ "Number of times to attempt a successful test run in Instruments (reliability hack)") do |a|
81
+ options.attempts = a
82
+ end
83
+
84
+ opts.on("-s", "--startuptimeout DURATION", OptionParser::DecimalInteger,
85
+ "Amount of time to wait for output from Instruments scripts before judging it to be hung") do |t|
86
+ options.startup_timeout = t
87
+ end
88
+ end
89
+
90
+ unless ARGV.length >= 3
91
+ STDERR.puts opts_parser.help
92
+ exit 1
93
+ end
94
+
95
+ app_bundle, test_script, test_output = ARGV.shift 3
96
+
97
+ begin
98
+ opts_parser.parse!
99
+ rescue OptionParser::ParseError => e
100
+ puts e
101
+ puts opts_parser
102
+ exit 1
103
+ end
104
+
105
+ # instrument cli helper does not recognise any relative directory.
106
+ # expand the relative directory and throw an exception if it does not texist.
107
+ test_output = File.expand_path(test_output)
108
+ raise "Output directory #{test_output} does not exist" unless File.directory?(test_output)
109
+
110
+
111
+ # Output options
112
+ outputs = []
113
+ unless options.quiet # HINT this could be moved into the actual options parsing
114
+ outputs << (options.colorize ? ColoredConsoleOutput.new : (options.brief ? AbbreviatedConsoleOutput.new : ConsoleOutput.new))
115
+ end
116
+ if options.xunit
117
+ report_filename = File.join(test_output, File.basename(test_script, File.extname(test_script)) + ".xml")
118
+ outputs << XunitOutput.new(report_filename)
119
+ end
120
+
121
+
122
+
123
+ # Dynamic UDID
124
+ # Discussion: better to have a separate command line option for dynamic?
125
+ if options.device then
126
+ if options.device.length == 40
127
+ # UDID provided
128
+ elsif options.device == "dynamic"
129
+ ioreg_output = `ioreg -w 0 -rc IOUSBDevice -k SupportsIPhoneOS`
130
+ if ioreg_output =~ /"USB Serial Number" = "([0-9a-z]+)"/ then
131
+ options.device = $1
132
+ puts "Using device with UDID #{options.device}", "\n" if options.verbose
133
+ else
134
+ raise "Couldn't get the UDID using ioreg"
135
+ end
136
+ else
137
+ raise "Invalid UDID with length #{options.device.length} provided"
138
+ end
139
+ end
140
+
141
+ # Check for already running instrument processes
142
+ instrument_pids = `ps aux | grep -i instrument[s] | awk '{ print $2 }'`
143
+ if instrument_pids != "" then
144
+ warn "\n### There are other 'instrument' processes running that may interfere - consider terminating them first:\nkill -9 " + instrument_pids
145
+ end
146
+
147
+ # Find the bundle path at runtime
148
+ # Discussion: better to have a command line option for app_name,
149
+ # than using a "bare" app_bundle to specify app_name?
150
+ app_name = File.basename(app_bundle, ".*")
151
+ if app_bundle == app_name then
152
+ # List existing bundles, newest first
153
+ mdargs = 'kMDItemDisplayName == "' + app_name + '.app" || ' +
154
+ 'kMDItemAlternateNames == "' + app_name + '.app"'
155
+ bundle_paths = `mdfind -0 '#{mdargs}' | xargs -0 ls -dt`.split(/\n/)
156
+ if options.device
157
+ bundles = bundle_paths.grep(/\/(Debug|Release)-iphoneos\//)
158
+ else
159
+ bundles = bundle_paths.grep(/\/iPhone Simulator\/|\/(Debug|Release)-iphonesimulator\//)
160
+ end
161
+ if bundles.size > 0
162
+ app_bundle = bundles[0]
163
+ puts "Found relevant bundles (newest first):", bundles, "\n" if options.verbose
164
+ puts "Using bundle:", app_bundle, "\n"
165
+ else
166
+ STDERR.puts "Matching but irrelevant bundles:", bundle_paths if bundle_paths.size > 0
167
+ raise "No output bundles found for app #{app_name}"
168
+ end
169
+ end
170
+
171
+ # Instruments wants the test_script and app_bundle to be a fully-qualified path
172
+ test_script = File.expand_path(test_script)
173
+ raise "Test script '#{test_script}' doesn't exist" unless File.exist?(test_script)
174
+ app_bundle = File.expand_path(app_bundle)
175
+ raise "App bundle '#{app_bundle}' doesn't exist" unless File.directory?(app_bundle)
176
+
177
+
178
+
179
+ if options.preprocess or options.run
180
+ temp_file = Tempfile.new(File.basename(test_script))
181
+
182
+ # inject test filter
183
+ if options.run
184
+ puts "Injecting test filter to only run test \"#{options.run}\"", "\n" if options.verbose
185
+ temp_file.write("var TUNEUP_ONLY_RUN = [\"#{options.run}\"];\n\n")
186
+ end
187
+
188
+ # apply custom preprocessing to test_script
189
+ if options.preprocess
190
+ puts "Preprocessing file..." if options.verbose
191
+ temp_file.write(Preprocessor.process test_script)
192
+ puts "contents written to temp file #{temp_file.path}", "\n" if options.verbose
193
+ end
194
+
195
+ temp_file.flush
196
+ test_script = temp_file.path
197
+ end
198
+
199
+
200
+ SDKROOT = `/usr/bin/xcodebuild -version -sdk iphoneos | grep PlatformPath`.split(":")[1].chomp.sub(/^\s+/, "")
201
+ XCODE_ROOT = `/usr/bin/xcode-select -print-path`.chomp.sub(/^\s+/, "")
202
+
203
+ # path is different for Instruments 6.0
204
+ if File.directory? "#{XCODE_ROOT}/../Applications/Instruments.app/Contents/PlugIns/AutomationInstrument.xrplugin/"
205
+ instruments_folder = "AutomationInstrument.xrplugin";
206
+ else
207
+ # older instruments path
208
+ instruments_folder = "AutomationInstrument.bundle";
209
+ end
210
+
211
+
212
+ TEMPLATE = `[ -f /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Instruments/PlugIns/#{instruments_folder}/Contents/Resources/Automation.tracetemplate ] && echo "#{SDKROOT}/Developer/Library/Instruments/PlugIns/#{instruments_folder}/Contents/Resources/Automation.tracetemplate" || echo "#{XCODE_ROOT}/../Applications/Instruments.app/Contents/PlugIns/#{instruments_folder}/Contents/Resources/Automation.tracetemplate"`.chomp.sub(/^\s+/, "")
213
+
214
+ command = ["env", "DEVELOPER_DIR=#{XCODE_ROOT}", "/usr/bin/instruments"]
215
+ if options.device
216
+ command << "-w" << options.device
217
+ elsif options.simulatorName
218
+ command << "-w" << options.simulatorName
219
+ end
220
+
221
+ command << "-t" << "#{TEMPLATE}"
222
+ command << app_bundle
223
+ command << "-e" << "UIASCRIPT" << test_script
224
+ command << "-e" << "UIARESULTSPATH" << test_output
225
+ options.env_vars.to_a.each do |pair|
226
+ command << "-e"
227
+ command.concat(pair)
228
+ end
229
+ command << options.language if options.language
230
+ puts "command=" + command.map { |s| s.sub(/\A(.*\s.*)\Z/, '"\1"') }.join(' ') if options.verbose
231
+
232
+
233
+ FileUtils.mkdir_p test_output unless File.directory? test_output
234
+
235
+
236
+ def parse_status(status)
237
+ case status
238
+ when /start/i then :start
239
+ when /pass/i then :pass
240
+ when /fail/i then :fail
241
+ when /error/i then :error
242
+ when /warning/i then :warning
243
+ when /issue/i then :issue
244
+ when /default/i then :default
245
+ else nil
246
+ end
247
+ end
248
+
249
+ def failed?(statistics)
250
+ statistics[:total].to_i == 0 || statistics[:fail].to_i > 0 || statistics[:error].to_i > 0
251
+ end
252
+
253
+ def format_statistics(statistics)
254
+ output = "#{statistics[:total].to_i} tests, #{statistics[:fail].to_i} failures"
255
+ output << ", #{statistics[:error].to_i} errors" if statistics[:error].to_i > 0
256
+ output << ", #{statistics[:warning].to_i} warnings" if statistics[:warning].to_i > 0
257
+ output << ", #{statistics[:issue].to_i} issues" if statistics[:issue].to_i > 0
258
+ output
259
+ end
260
+
261
+ statistics = {}
262
+ failed = false
263
+ started = false
264
+ remaining_attempts = options.attempts
265
+
266
+ while (not started) && remaining_attempts > 0 do
267
+ statistics = {}
268
+ failed = false
269
+ remaining_attempts = remaining_attempts - 1
270
+ warn "\n### Launching instruments. Will retry #{remaining_attempts} more times if it doesn't initialize"
271
+
272
+ begin
273
+ Dir.chdir(test_output) do
274
+
275
+ PTY.spawn(*command) do |r, w, pid|
276
+ while not failed do
277
+ if not started then
278
+ mytimeout = options.startup_timeout
279
+ else
280
+ mytimeout = options.timeout
281
+ end
282
+ if IO.select([r], nil, nil, mytimeout) then
283
+ line = r.readline.rstrip
284
+ if (line.include? " +0000 ") && (not line.include? " +0000 Fail: The target application appears to have died") then
285
+ started = true
286
+ end
287
+
288
+ _, date, time, tz, status, msg = line.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) ([+-]\d{4}) ([^:]+): (.*)$/).to_a
289
+
290
+ if status
291
+ if options.verbose || status != "Debug"
292
+ status = parse_status(status) if status
293
+ outputs.each { |output| output.add_status(status, date, time, tz, msg) }
294
+ statistics[:total] = statistics[:total].to_i + 1 if status == :start
295
+ statistics[status] = statistics[status].to_i + 1
296
+ end
297
+ else
298
+ outputs.each { |output| output.add(line) } if options.verbose
299
+ end
300
+
301
+ failed = true if line =~ /Instruments Trace Error/i
302
+ else
303
+ failed = true
304
+ warn "\n### Timeout #{options.timeout} reached without any output - " +
305
+ "killing Instruments (pid #{pid})..."
306
+ begin
307
+ Process.kill(9, pid)
308
+ w.close
309
+ r.close
310
+ Process.wait(pid)
311
+ rescue PTY::ChildExited
312
+ end
313
+ puts "Pid #{pid} killed."
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ rescue Errno::EIO
320
+ rescue Errno::ECHILD
321
+ rescue EOFError
322
+ rescue PTY::ChildExited
323
+ STDERR.puts "Instruments exited unexpectedly"
324
+ exit 1 if started
325
+ ensure
326
+ outputs.map(&:close)
327
+
328
+ if temp_file
329
+ temp_file.close
330
+ temp_file.unlink
331
+ puts "Deleted temp file" if options.verbose
332
+ end
333
+ end
334
+ end
335
+
336
+ if failed || failed?(statistics)
337
+ STDERR.puts format_statistics(statistics)
338
+ STDERR.puts "#{test_script} failed, see log output for details"
339
+ exit 1
340
+ else
341
+ STDOUT.puts format_statistics(statistics)
342
+ STDOUT.puts "TEST PASSED"
343
+ end
@@ -0,0 +1,114 @@
1
+ require 'date'
2
+
3
+ class TestSuite
4
+ attr_reader :name, :timestamp
5
+ attr_accessor :test_cases
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ @test_cases = []
10
+ @timestamp = DateTime.now
11
+ end
12
+
13
+ def failures
14
+ @test_cases.count { |test| test.failed? }
15
+ end
16
+
17
+ def time
18
+ @test_cases.map { |test| test.time }.inject(:+)
19
+ end
20
+ end
21
+
22
+ class TestCase
23
+ attr_reader :name
24
+ attr_accessor :messages
25
+
26
+ def initialize(name)
27
+ @name = name
28
+ @messages = []
29
+ @failed = true
30
+ @start = Time.now
31
+ @finish = nil
32
+ end
33
+
34
+ def <<(message)
35
+ @messages << message
36
+ end
37
+
38
+ def pass!
39
+ @failed = false;
40
+ @finish = Time.now
41
+ end
42
+
43
+ def fail!
44
+ @finish = Time.now
45
+ end
46
+
47
+ def failed?
48
+ @failed
49
+ end
50
+
51
+ def time
52
+ return 0 if @finish.nil?
53
+ @finish - @start
54
+ end
55
+ end
56
+
57
+ # Creates a XML report that conforms to # https://svn.jenkins-ci.org/trunk/hudson/dtkit/dtkit-format/dtkit-junit-model/src/main/resources/com/thalesgroup/dtkit/junit/model/xsd/junit-4.xsd
58
+ class XunitOutput
59
+ def initialize(filename)
60
+ @filename = filename
61
+ @suite = TestSuite.new(File.basename(filename, File.extname(filename)))
62
+ end
63
+
64
+ def add(line)
65
+ return if @suite.test_cases.empty?
66
+ @suite.test_cases.last << line
67
+ end
68
+
69
+ def add_status(status, date, time, time_zone, msg)
70
+ case status
71
+ when :start
72
+ @suite.test_cases << TestCase.new(msg)
73
+ when :pass
74
+ @suite.test_cases.last.pass! if @suite.test_cases.last != nil
75
+ when :fail
76
+ @suite.test_cases.last.fail! if @suite.test_cases.last != nil
77
+ else
78
+ if @suite.test_cases.last != nil && @suite.test_cases.last.time == 0
79
+ @suite.test_cases.last << "#{status.to_s.capitalize}: #{msg}"
80
+ end
81
+ end
82
+ end
83
+
84
+ def close
85
+ File.open(@filename, 'w') { |f| f.write(serialize(@suite)) }
86
+ end
87
+
88
+ def xml_escape(input)
89
+ result = input.dup
90
+
91
+ result.gsub!("&", "&amp;")
92
+ result.gsub!("<", "&lt;")
93
+ result.gsub!(">", "&gt;")
94
+ result.gsub!("'", "&apos;")
95
+ result.gsub!("\"", "&quot;")
96
+
97
+ return result
98
+ end
99
+
100
+ def serialize(suite)
101
+ output = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" << "\n"
102
+ output << "<testsuite name=\"#{xml_escape(suite.name)}\" timestamp=\"#{suite.timestamp}\" time=\"#{suite.time}\" tests=\"#{suite.test_cases.count}\" failures=\"#{suite.failures}\">" << "\n"
103
+
104
+ suite.test_cases.each do |test|
105
+ output << " <testcase name=\"#{xml_escape(test.name)}\" time=\"#{test.time}\">" << "\n"
106
+ if test.failed?
107
+ output << " <failure>#{test.messages.map { |m| xml_escape(m) }.join("\n")}</failure>" << "\n"
108
+ end
109
+ output << " </testcase>" << "\n"
110
+ end
111
+
112
+ output << "</testsuite>" << "\n"
113
+ end
114
+ end