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.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +77 -0
- data/Rakefile +57 -0
- data/Troubleshooting.md +61 -0
- data/VERSION +1 -0
- data/bin/smart_monkey +53 -0
- data/lib/bootstrap/css/bootstrap-responsive.css +1109 -0
- data/lib/bootstrap/css/bootstrap-responsive.min.css +9 -0
- data/lib/bootstrap/css/bootstrap.css +6167 -0
- data/lib/bootstrap/css/bootstrap.min.css +9 -0
- data/lib/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/lib/bootstrap/img/glyphicons-halflings.png +0 -0
- data/lib/bootstrap/js/bootstrap.js +2280 -0
- data/lib/bootstrap/js/bootstrap.min.js +6 -0
- data/lib/ios_device_log/deviceconsole +0 -0
- data/lib/smart_monkey.rb +2 -0
- data/lib/smart_monkey/command_helper.rb +71 -0
- data/lib/smart_monkey/monkey_runner.rb +549 -0
- data/lib/smart_monkey/templates/automation_result.xsl +61 -0
- data/lib/smart_monkey/templates/index.html.erb +77 -0
- data/lib/smart_monkey/templates/result.html.erb +110 -0
- data/lib/smart_monkey/templates/result_view.coffee +160 -0
- data/lib/smart_monkey/templates/result_view.js +250 -0
- data/lib/ui-auto-monkey/UIAutoMonkey.js +470 -0
- data/lib/ui-auto-monkey/custom.js +73 -0
- data/lib/ui-auto-monkey/handler/buttonHandler.js +111 -0
- data/lib/ui-auto-monkey/handler/wbScrollViewButtonHandler.js +114 -0
- data/lib/ui-auto-monkey/tuneup/LICENSE +20 -0
- data/lib/ui-auto-monkey/tuneup/assertions.js +402 -0
- data/lib/ui-auto-monkey/tuneup/image_asserter +26 -0
- data/lib/ui-auto-monkey/tuneup/image_assertion.js +65 -0
- data/lib/ui-auto-monkey/tuneup/image_assertion.rb +102 -0
- data/lib/ui-auto-monkey/tuneup/lang-ext.js +76 -0
- data/lib/ui-auto-monkey/tuneup/screen.js +11 -0
- data/lib/ui-auto-monkey/tuneup/test.js +71 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/abbreviated_console_output.rb +38 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/colored_console_output.rb +27 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/console_output.rb +17 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/preprocessor.rb +25 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/run +343 -0
- data/lib/ui-auto-monkey/tuneup/test_runner/xunit_output.rb +114 -0
- data/lib/ui-auto-monkey/tuneup/tuneup.js +6 -0
- data/lib/ui-auto-monkey/tuneup/tuneup_js.podspec +52 -0
- data/lib/ui-auto-monkey/tuneup/uiautomation-ext.js +965 -0
- data/smart_monkey.gemspec +112 -0
- data/spec/spec_helper.rb +12 -0
- 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!("&", "&")
|
92
|
+
result.gsub!("<", "<")
|
93
|
+
result.gsub!(">", ">")
|
94
|
+
result.gsub!("'", "'")
|
95
|
+
result.gsub!("\"", """)
|
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
|