initial import
[vuplus_webkit] / Tools / iExploder / iexploder-1.7.2 / src / browser_harness.rb
1 #!/usr/bin/ruby
2 # iExploder browser Harness  (test a single web browser)
3 #
4 # Copyright 2010 Thomas Stromberg - All Rights Reserved.
5 #
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 #      http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 #
18 #----------------------------------------------------------------------------
19 # PLEASE NOTE:
20 #
21 # You must disable automatic session restoring for this to be useful.
22 #
23 # chrome --incognito
24 # opera --nosession -newprivatetab
25 # firefox -private
26 require 'cgi'
27 require 'open-uri'
28 require 'optparse'
29 require './iexploder.rb'
30 require './scanner.rb'
31
32 MAC_CRASH_PATH = "#{ENV['HOME']}/Library/Logs/CrashReporter"
33 TESTCASE_URL = "http://127.0.0.1:3100/iexploder.cgi"
34
35 class BrowserHarness
36   def initialize(port, config_path, log_dir, test_dir, watchdog_timer, scan_timer)
37     @app_base_url = "http://127.0.0.1:#{port}/"
38     @app_url = "#{@app_base_url}iexploder.cgi"
39     @port = port
40     @log_dir = log_dir
41     @server_log_path = "#{log_dir}/iexploder_webserver-#{port}.log"
42     @client_log_path = "#{log_dir}/iexploder_harness-#{port}.log"
43     @test_dir = test_dir
44     @watchdog_timer = watchdog_timer
45     @scan_timer = scan_timer
46     @config_path = config_path
47
48     @ie = IExploder.new(@config_path)
49     @ie.cgi_url = @app_url
50
51     @browser_id = nil
52     @browser_name = nil
53     msg("Client log: #{@client_log_path}")
54     msg("Server log: #{@server_log_path}")
55     @server_pid = launch_server()
56   end
57
58   def msg(text)
59     now = Time.now()
60     msg = ">>> #{@browser_name}:#{@port} | #{now}: #{text}"
61     puts msg
62     STDOUT.flush
63
64     f = File.open(@client_log_path, 'a')
65     f.puts msg
66     f.close
67   end
68
69   def launch_server()
70     args = ['./webserver.rb', "-p#{@port}", "-c#{@config_path}", "-l#{@server_log_path}"]
71     pids = fork { exec(*args) }
72     msg("Server args: #{args.inspect}")
73     msg("Server pid: #{pids.inspect}")
74     return pids
75   end
76
77   def launch_browser(args, url)
78     if ! File.exist?(args[0])
79       msg("First argument does not appear to be an executable file: #{args[0]}")
80       kill_server()
81       exit
82     end
83
84     browser = File.basename(args[0])
85     @browser_name = File.basename(browser)
86     if browser =~ /\.app$/
87       pids = launch_mac_browser(args, url)
88     else
89       pids = launch_posix_browser(args, url)
90     end
91     sleep(@scan_timer * 3)
92     if ! File.size?(@server_log_path)
93       puts "#{@server_log_path} was never written to. Unable to launch browser?"
94       kill_server()
95       exit
96     end
97     return pids
98   end
99
100   def launch_posix_browser(args, url)
101     browser = File.basename(args[0])
102     msg("Killing browser processes: #{browser}")
103     system("pkill #{browser} && pkill -9 #{browser}")
104     args = args + [url]
105     msg("Launching browser: #{args.inspect}")
106     browser_pid = fork {
107       exec(*args)
108     }
109     return [browser_pid]
110   end
111
112   def find_pids(text)
113     # Only tested on Mac OS X.
114     pids = []
115     `ps -x`.each do |proc_line|
116       if proc_line =~ /^ *(\d+).*#{text}/
117         pid = $1.to_i
118         # Do not include yourself.
119         if pid != Process.pid
120           pids << $1.to_i
121         end
122       end
123     end
124     return pids
125   end
126
127   def launch_mac_browser(args, url)
128     # This is dedicated to Safari.
129     if args.length > 1
130       msg(".app type launches do not support arguments, ignoring #{args[1..99].inspect}")
131     end
132     browser = args[0]
133     pids = find_pids(browser)
134     if pids
135       kill_pids(find_pids(browser))
136       sleep(2)
137     end
138     command = "open -a \"#{browser}\" \"#{url}\""
139     msg(".app open command: #{command}")
140     system(command)
141     return find_pids(browser)
142   end
143
144   def kill_pids(pids)
145     pids.each do |pid|
146       msg("Killing #{pid}")
147       begin
148         Process.kill("INT", pid)
149         sleep(0.5)
150         Process.kill("KILL", pid)
151       rescue
152         sleep(0.1)
153       end
154     end
155   end
156
157   def encode_browser()
158     return @browser_id.gsub(' ', '_').gsub(';', '').gsub('/', '-').gsub(/[\(\):\!\@\#\$\%\^\&\*\+=\{\}\[\]\'\"\<\>\?\|\\]/, '').gsub(/_$/, '').gsub(/^_/, '')
159   end
160
161   def kill_server()
162     kill_pids([@server_pid])
163   end
164
165   def parse_test_url(value)
166     current_vars = nil
167     test_num = nil
168     subtest_data = nil
169     lookup_values = false
170     if value =~ /iexploder.cgi(.*)/
171       current_vars = $1
172       if current_vars =~ /[&\?]t=(\d+)/
173         test_num = $1
174       end
175       if current_vars =~ /[&\?]s=([\d_,]+)/
176         subtest_data = $1
177       end
178       if current_vars =~ /[&\?]l=(\w+)/
179         lookup_value = $1
180       end
181     else
182       msg("Unable to parse url in #{value}")
183       return [nil, nil, nil, nil]
184     end
185     return [current_vars, test_num, subtest_data, lookup_value]
186   end
187
188   def check_log_status()
189     timestamp, uri, user_agent = open("#{@app_base_url}last_page.cgi").read().chomp.split(' ')
190     age = (Time.now() - timestamp.to_i).to_i
191     if not @browser_id
192       @browser_id = CGI.unescape(user_agent)
193       msg("My browser is #{@browser_id}")
194     end
195
196
197     return [age, uri]
198   end
199
200   def save_testcase(url, case_type=nil)
201     msg("Saving testcase: #{url}")
202     vars, test_num, subtest_data, lookup_value = parse_test_url(url)
203     if not case_type
204       case_type = 'testcase'
205     end
206
207     testcase_name = ([case_type, encode_browser(), 'TEST', test_num, subtest_data].join('-')).gsub(/-$/, '') + ".html"
208     testcase_path = "#{@test_dir}/#{testcase_name}"
209     data = open(url).read()
210     # Slow down our redirection time, and replace our testcase urls.
211     data.gsub!(/0;URL=\/iexploder.*?\"/, "1;URL=#{testcase_name}\"")
212     data.gsub!(/window\.location=\"\/iexploder.*?\"/, "window\.location=\"#{testcase_name}\"")
213
214     # I wish I did not have to do this, but the reality is that I can't imitate header fuzzing
215     # without a webservice in the backend. Change all URL's to use a well known localhost
216     # port.
217     data.gsub!(/\/iexploder.cgi/, TESTCASE_URL)
218
219     f = File.open(testcase_path, 'w')
220     f.write(data)
221     f.close
222     msg("Wrote testcase #{testcase_path}")
223     return testcase_path
224   end
225
226   def calculate_next_url(test_num, subtest_data)
227     @ie.test_num = test_num.to_i
228     @ie.subtest_data = subtest_data
229     if subtest_data and subtest_data.length > 0
230       (width, offsets) = @ie.parseSubTestData(subtest_data)
231       # We increment within combo_creator
232       (width, offsets, lines) = combine_combo_creator(@ie.config['html_tags_per_page'], width, offsets)
233       return @ie.generateTestUrl(@ie.nextTestNum(), width, offsets)
234     else
235       return @ie.generateTestUrl(@ie.nextTestNum())
236     end
237   end
238
239   def find_crash_logs(max_age)
240     crashed_files = []
241     check_files = Dir.glob("*core*")
242     if File.exists?(MAC_CRASH_PATH)
243       check_files = check_files + Dir.glob("#{MAC_CRASH_PATH}/*.*")
244     end
245     check_files.each do |file|
246       mtime = File.stat(file).mtime
247       age = (Time.now() - mtime).to_i
248       if age < max_age
249         msg("#{file} is only #{age}s old: #{mtime}")
250         crashed_files << file
251       end
252     end
253     return crashed_files
254   end
255
256   def test_browser(args, test_num, random_mode=false)
257     # NOTE: random_mode is not yet supported.
258
259     browser_pids = []
260     subtest_data = nil
261     @ie.test_num = test_num
262     @ie.random_mode = random_mode
263     next_url = @ie.generateTestUrl(test_num)
264
265     while next_url
266       msg("Starting at: #{next_url}")
267       if browser_pids
268         kill_pids(browser_pids)
269       end
270       browser_pids = launch_browser(args, next_url)
271       test_is_running = true
272       crash_files = []
273
274       while test_is_running
275         sleep(@scan_timer)
276         begin
277           age, request_uri = check_log_status()
278         rescue
279           msg("Failed to get status. webserver likely crashed.")
280           kill_pids([@server_pid])
281           @server_pid = launch_server()
282           next_url = @ie.generateTestUrl(test_num)
283           test_is_running = false
284           next
285         end
286         vars, test_num, subtest_data, lookup_value = parse_test_url(request_uri)
287         if lookup_value == 'survived_redirect'
288           msg("We survived #{vars}. Bummer, could not repeat crash. Moving on.")
289           test_is_running = false
290           next_url = calculate_next_url(test_num, subtest_data)
291           next
292         elsif age > @watchdog_timer
293           msg("Stuck at #{vars}, waited for #{@watchdog_timer}s. Killing browser.")
294           kill_pids(browser_pids)
295           current_url = "#{@app_url}#{vars}"
296 #          save_testcase(current_url, 'possible')
297           crash_files = find_crash_logs(@watchdog_timer + (@scan_timer * 2))
298           if crash_files.length > 0
299             msg("Found recent crash logs: #{crash_files.inspect} - last page: #{current_url}")
300           end
301
302           if vars =~ /THE_END/
303             msg("We hung at the end. Saving a testcase just in case.")
304             save_testcase(current_url)
305             next_url = calculate_next_url(test_num, nil)
306             test_is_running = false
307             next
308           end
309
310           # This is for subtesting
311           if subtest_data
312             if lookup_value
313               msg("Confirmed crashing/hanging page at #{current_url} - saving testcase.")
314               save_testcase(current_url)
315               next_url = calculate_next_url(test_num, nil)
316               test_is_running = false
317               next
318             else
319               msg("Stopped at #{current_url}. Attempting to reproduce simplified crash/hang condition.")
320               browser_pids = launch_browser(args, "#{current_url}&l=test_redirect")
321             end
322           # Normal testing goes here
323           else
324             if lookup_value
325               msg("Reproducible crash/hang at #{current_url}, generating smaller test case.")
326               url = current_url.gsub(/&l=(\w+)/, '')
327               browser_pids = launch_browser(args, "#{url}&s=0")
328             else
329               msg("Stopped at #{current_url}. Attempting to reproduce crash/hang condition.")
330               browser_pids = launch_browser(args, "#{current_url}&l=test_redirect")
331             end
332           end
333         elsif age > @scan_timer
334           msg("Waiting for #{vars} to finish loading... (#{age}s of #{@watchdog_timer}s)")
335         end
336       end
337     end
338   end
339 end
340
341 if $0 == __FILE__
342   options = {
343     :port => rand(16000).to_i + 16000,
344     :test_dir => File.dirname($0) + '/../output',
345     :log_dir => File.dirname($0) + '/../output',
346     :test_num => nil,
347     :watchdog_timer => 60,
348     :scan_timer => 5,
349     :config_path => 'config.yaml',
350     :random_mode => false
351   }
352
353   optparse = OptionParser.new do |opts|
354     opts.banner = "Usage: browser_harness.rb [options] -- <browser path> <browser options>"
355     opts.on( '-t', '--test NUM', 'Test to start at' ) { |test_num| options[:test_num] = test_num.to_i }
356     opts.on( '-p', '--port NUM', 'Listen on TCP port NUM (random)' ) { |port| options[:port] = port.to_i }
357     opts.on( '-c', '--config PATH', 'Use PATH for configuration file' ) { |path| options[:config_path] = path }
358     opts.on( '-d', '--testdir PATH', 'Use PATH to save testcases (/tmp)' ) { |path| options[:test_dir] = path }
359     opts.on( '-l', '--logdir PATH', 'Use PATH to save logs (/tmp)' ) { |path|  options[:log_dir] = path }
360     opts.on( '-w', '--watchdog NUM', 'How many seconds to wait for pages to load (45s)' ) { |sec|  options[:watchdog_timer] = sec.to_i }
361     opts.on( '-r', '--random', 'Generate test numbers pseudo-randomly' ) { options[:random_mode] = true }
362     opts.on( '-s', '--scan NUM', 'How often to check for new log data (5s)' ) { |sec|  options[:scan_timer] = sec.to_i }
363     opts.on( '-h', '--help', 'Display this screen' ) { puts opts; exit }
364   end
365   optparse.parse!
366
367   if options[:port] == 0
368     puts "Unable to parse port option. Try adding -- as an argument before you specify your browser location."
369     exit
370   end
371
372   if ARGV.length < 1
373     puts "No browser specified. Perhaps you need some --help?"
374     exit
375   end
376   puts "options: #{options.inspect}"
377   puts "browser: #{ARGV.inspect}"
378
379   harness = BrowserHarness.new(
380     options[:port],
381     options[:config_path],
382     options[:log_dir],
383     options[:test_dir],
384     options[:watchdog_timer],
385     options[:scan_timer]
386   )
387
388   harness.test_browser(ARGV, options[:test_num], options[:random_mode])
389 end