smart_monkey 0.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.
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