jtzemp-tourbus 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +22 -0
- data/README.rdoc +200 -0
- data/bin/tourbus +46 -0
- data/bin/tourwatch +52 -0
- data/examples/contact_app/README.rdoc +134 -0
- data/examples/contact_app/contact_app.rb +36 -0
- data/examples/contact_app/tours/simple.rb +20 -0
- data/examples/contact_app/tours/tourbus.yml +2 -0
- data/lib/common.rb +31 -0
- data/lib/runner.rb +72 -0
- data/lib/tour.rb +145 -0
- data/lib/tour_bus.rb +92 -0
- data/lib/tour_watch.rb +88 -0
- data/lib/web-sickle/init.rb +17 -0
- data/lib/web-sickle/lib/assertions.rb +51 -0
- data/lib/web-sickle/lib/hash_proxy.rb +9 -0
- data/lib/web-sickle/lib/helpers/asp_net.rb +16 -0
- data/lib/web-sickle/lib/helpers/table_reader.rb +39 -0
- data/lib/web-sickle/lib/make_nokigiri_output_useful.rb +15 -0
- data/lib/web-sickle/lib/web_sickle.rb +227 -0
- data/lib/web-sickle/spec/lib/helpers/table_reader_spec.rb +137 -0
- data/lib/web-sickle/spec/spec_helper.rb +7 -0
- data/lib/web-sickle/spec/spec_helpers/mechanize_mock_helper.rb +12 -0
- data/lib/web-sickle/spec/web_sickle_spec.rb +50 -0
- data/lib/web_sickle_webrat_adapter.rb +40 -0
- metadata +125 -0
data/lib/tour.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'monitor'
|
3
|
+
require 'common'
|
4
|
+
require 'webrat'
|
5
|
+
require 'webrat/mechanize'
|
6
|
+
require 'test/unit/assertions'
|
7
|
+
|
8
|
+
# A tour is essentially a test suite file. A Tour subclass
|
9
|
+
# encapsulates a set of tests that can be done, and may contain helper
|
10
|
+
# and support methods for a given task. If you have a two or three
|
11
|
+
# paths through a specific area of your website, define a tour for
|
12
|
+
# that area and create test_ methods for each type of test to be done.
|
13
|
+
|
14
|
+
class Tour
|
15
|
+
extend Forwardable
|
16
|
+
include Webrat::Matchers
|
17
|
+
include Webrat::SaveAndOpenPage
|
18
|
+
include Test::Unit::Assertions
|
19
|
+
|
20
|
+
attr_reader :host, :tours, :number, :tour_type, :tour_id, :webrat_session
|
21
|
+
|
22
|
+
# delegate goodness to webrat
|
23
|
+
[
|
24
|
+
:fill_in,
|
25
|
+
:fills_in,
|
26
|
+
:set_hidden_field,
|
27
|
+
:submit_form,
|
28
|
+
:check,
|
29
|
+
:checks,
|
30
|
+
:uncheck,
|
31
|
+
:unchecks,
|
32
|
+
:choose,
|
33
|
+
:chooses,
|
34
|
+
:select,
|
35
|
+
:selects,
|
36
|
+
:select_datetime,
|
37
|
+
:selects_datetime,
|
38
|
+
:select_date,
|
39
|
+
:selects_date,
|
40
|
+
:select_time,
|
41
|
+
:selects_time,
|
42
|
+
:attach_file,
|
43
|
+
:attaches_file,
|
44
|
+
:click_area,
|
45
|
+
:clicks_area,
|
46
|
+
:click_link,
|
47
|
+
:clicks_link,
|
48
|
+
:click_button,
|
49
|
+
:clicks_button,
|
50
|
+
:field_labeled,
|
51
|
+
:field_by_xpath,
|
52
|
+
:field_with_id,
|
53
|
+
:select_option,
|
54
|
+
:automate,
|
55
|
+
:basic_auth,
|
56
|
+
:check_for_infinite_redirects,
|
57
|
+
:click_link_within,
|
58
|
+
:dom,
|
59
|
+
:header,
|
60
|
+
:http_accept,
|
61
|
+
:infinite_redirect_limit_exceeded?,
|
62
|
+
:internal_redirect?,
|
63
|
+
:redirected_to,
|
64
|
+
:reload,
|
65
|
+
:response_body,
|
66
|
+
:simulate,
|
67
|
+
:visit,
|
68
|
+
:within,
|
69
|
+
:xml_content_type?].each {|m| def_delegators(:webrat_session, m) }
|
70
|
+
|
71
|
+
def initialize(host, tours, number, tour_id)
|
72
|
+
@host, @tours, @number, @tour_id = host, tours, number, tour_id
|
73
|
+
@tour_type = self.send(:class).to_s
|
74
|
+
@webrat_session = Webrat::MechanizeSession.new
|
75
|
+
end
|
76
|
+
|
77
|
+
# before_tour runs once per tour, before any tests get run
|
78
|
+
def before_tour; end
|
79
|
+
|
80
|
+
# after_tour runs once per tour, after all the tests have run
|
81
|
+
def after_tour; end
|
82
|
+
|
83
|
+
def setup
|
84
|
+
end
|
85
|
+
|
86
|
+
def teardown
|
87
|
+
end
|
88
|
+
|
89
|
+
def wait(time)
|
90
|
+
sleep time.to_i
|
91
|
+
end
|
92
|
+
|
93
|
+
# Lists tours in tours folder. If a string is given, filters the
|
94
|
+
# list by that string. If an array of filter strings is given,
|
95
|
+
# returns items that match ANY filter string in the array.
|
96
|
+
def self.tours(filter=[])
|
97
|
+
filter = [filter].flatten
|
98
|
+
# All files in tours folder, stripped to basename, that match any item in filter
|
99
|
+
# I do loves me a long chain. This returns an array containing
|
100
|
+
# 1. All *.rb files in tour folder (recursive)
|
101
|
+
# 2. Each filename stripped to its basename
|
102
|
+
# 3. If you passed in any filters, these basenames are rejected unless they match at least one filter
|
103
|
+
# 4. The filenames remaining are then checked to see if they define a class of the same name that inherits from Tour
|
104
|
+
Dir[File.join('.', 'tours', '**', '*.rb')].map {|fn| File.basename(fn, ".rb")}.select {|fn| filter.size.zero? || filter.any?{|f| fn =~ /#{f}/}}.select {|tour| Tour.tour? tour }
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.tests(tour_name)
|
108
|
+
Tour.make_tour(tour_name).tests
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.tour?(tour_name)
|
112
|
+
Object.const_defined?(tour_name.classify) && tour_name.classify.constantize.ancestors.include?(Tour)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Factory method, creates the named child class instance
|
116
|
+
def self.make_tour(tour_name,host="localhost:3000",tours=[],number=1,tour_id=nil)
|
117
|
+
tour_name.classify.constantize.new(host,tours,number,tour_id)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns list of tests in this tour. (Meant to be run on a subclass
|
121
|
+
# instance; returns the list of tests available).
|
122
|
+
def tests
|
123
|
+
methods.grep(/^test_/).map {|m| m.sub(/^test_/,'')}
|
124
|
+
end
|
125
|
+
|
126
|
+
def run_test(test_name)
|
127
|
+
@test = "test_#{test_name}"
|
128
|
+
raise TourBusException.new("run_test couldn't run test '#{test_name}' because this tour did not respond to :#{@test}") unless respond_to? @test
|
129
|
+
setup
|
130
|
+
send @test
|
131
|
+
teardown
|
132
|
+
end
|
133
|
+
|
134
|
+
protected
|
135
|
+
|
136
|
+
def session
|
137
|
+
@session ||= Webrat::MechanizeSession.new
|
138
|
+
end
|
139
|
+
|
140
|
+
def log(message)
|
141
|
+
puts "#{Time.now.strftime('%F %H:%M:%S')} Tour ##{@tour_id}: (#{@test}) #{message}"
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
data/lib/tour_bus.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
3
|
+
class TourBus < Monitor
|
4
|
+
attr_reader :host, :concurrency, :number, :tours, :runs, :tests, :passes, :fails, :errors, :benchmarks
|
5
|
+
|
6
|
+
def initialize(host="localhost", concurrency=1, number=1, tours=[], test_list=nil)
|
7
|
+
@host, @concurrency, @number, @tours, @test_list = host, concurrency, number, tours, test_list
|
8
|
+
@runner_id = 0
|
9
|
+
@runs, @tests, @passes, @fails, @errors = 0,0,0,0,0
|
10
|
+
super()
|
11
|
+
end
|
12
|
+
|
13
|
+
def next_runner_id
|
14
|
+
synchronize do
|
15
|
+
@runner_id += 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_stats(runs,tests,passes,fails,errors)
|
20
|
+
synchronize do
|
21
|
+
@runs += runs
|
22
|
+
@tests += tests
|
23
|
+
@passes += passes
|
24
|
+
@fails += fails
|
25
|
+
@errors += errors
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def update_benchmarks(bm)
|
30
|
+
synchronize do
|
31
|
+
@benchmarks = @benchmarks.zip(bm).map { |a,b| a+b}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def runners(filter=[])
|
36
|
+
# All files in tours folder, stripped to basename, that match any item in filter
|
37
|
+
Dir[File.join('.', 'tours', '**', '*.rb')].map {|fn| File.basename(fn, ".rb")}.select {|fn| filter.size.zero? || filter.any?{|f| fn =~ /#{f}/}}
|
38
|
+
end
|
39
|
+
|
40
|
+
def total_runs
|
41
|
+
tours.size * concurrency * number
|
42
|
+
end
|
43
|
+
|
44
|
+
def run
|
45
|
+
threads = []
|
46
|
+
tour_name = "#{total_runs} runs: #{concurrency}x#{number} of #{tours * ','}"
|
47
|
+
started = Time.now.to_f
|
48
|
+
concurrency.times do |conc|
|
49
|
+
log "Starting #{tour_name}"
|
50
|
+
threads << Thread.new do
|
51
|
+
runner_id = next_runner_id
|
52
|
+
runs,tests,passes,fails,errors,start = 0,0,0,0,0,Time.now.to_f
|
53
|
+
bm = Benchmark.measure do
|
54
|
+
runner = Runner.new(@host, @tours, @number, runner_id, @test_list)
|
55
|
+
runs,tests,passes,fails,errors = runner.run_tours
|
56
|
+
update_stats runs, tests, passes, fails, errors
|
57
|
+
end
|
58
|
+
log "Runner Finished!"
|
59
|
+
log "Runner finished in %0.3f seconds" % (Time.now.to_f - start)
|
60
|
+
log "Runner Finished! runs,passes,fails,errors: #{runs},#{passes},#{fails},#{errors}"
|
61
|
+
log "Benchmark for runner #{runner_id}: #{bm}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
log "All Runners started!"
|
65
|
+
threads.each {|t| t.join }
|
66
|
+
finished = Time.now.to_f
|
67
|
+
log '-' * 80
|
68
|
+
log tour_name
|
69
|
+
log "All Runners finished."
|
70
|
+
log "Total Tours: #{@runs}"
|
71
|
+
log "Total Tests: #{@tests}"
|
72
|
+
log "Total Passes: #{@passes}"
|
73
|
+
log "Total Fails: #{@fails}"
|
74
|
+
log "Total Errors: #{@errors}"
|
75
|
+
log "Elapsed Time: #{finished - started}"
|
76
|
+
log "Speed: %5.3f tours/sec" % (@runs / (finished-started))
|
77
|
+
log '-' * 80
|
78
|
+
if @fails > 0 || @errors > 0
|
79
|
+
log '********************************************************************************'
|
80
|
+
log '********************************************************************************'
|
81
|
+
log ' !! THERE WERE FAILURES !!'
|
82
|
+
log '********************************************************************************'
|
83
|
+
log '********************************************************************************'
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def log(message)
|
88
|
+
puts "#{Time.now.strftime('%F %H:%M:%S')} TourBus: #{message}"
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
data/lib/tour_watch.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
class TourWatch
|
2
|
+
attr_reader :processes
|
3
|
+
|
4
|
+
def initialize(options={})
|
5
|
+
@processes = if options[:processes]
|
6
|
+
options[:processes].split(/,/) * '|'
|
7
|
+
else
|
8
|
+
"ruby|mysql|apache|http|rails|mongrel"
|
9
|
+
end
|
10
|
+
@cores = options[:cores] || 4
|
11
|
+
@logfile = options[:outfile]
|
12
|
+
@mac = options[:mac]
|
13
|
+
end
|
14
|
+
|
15
|
+
def stats
|
16
|
+
top = @mac ? top_mac : top_linux
|
17
|
+
lines = []
|
18
|
+
@longest = Hash.new(0)
|
19
|
+
top.each_line do |line|
|
20
|
+
name,pid,cpu = fields(line.split(/\s+/))
|
21
|
+
lines << [name,pid,cpu]
|
22
|
+
@longest[:name] = name.size if name.size > @longest[:name]
|
23
|
+
@longest[:pid] = pid.to_s.size if pid.to_s.size > @longest[:pid]
|
24
|
+
end
|
25
|
+
lines
|
26
|
+
end
|
27
|
+
|
28
|
+
def fields(parts)
|
29
|
+
@mac ? fields_mac(parts) : fields_linux(parts)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Note: MacOSX is so awesome I just cacked. Top will report 0.0% cpu
|
33
|
+
# the first time you run top, every time. The only way to get actual
|
34
|
+
# CPU% here is to wait for it to send another page and then throw
|
35
|
+
# away the first page. Isn't that just awesome?!? I KNOW!!!
|
36
|
+
def top_mac
|
37
|
+
top = `top -l 1 | grep -E '(#{@processes})'`
|
38
|
+
end
|
39
|
+
|
40
|
+
def fields_mac(fields)
|
41
|
+
name,pid,cpu = fields[1], fields[0].to_i, fields[2].to_f
|
42
|
+
end
|
43
|
+
|
44
|
+
def top_linux
|
45
|
+
top = `top -bn 1 | grep -E '(#{@processes})'`
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def fields_linux(fields)
|
50
|
+
# linux top isn't much smarter. It spits out a blank field ahead
|
51
|
+
# of the pid if the pid is too short, which makes the indexes
|
52
|
+
# shift off by one.
|
53
|
+
a,b,c = if fields.size == 13
|
54
|
+
[-1,1,9]
|
55
|
+
else
|
56
|
+
[-1,0,8]
|
57
|
+
end
|
58
|
+
name,pid,cpu = fields[a], fields[b].to_i, fields[c].to_f
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def run()
|
63
|
+
while(true)
|
64
|
+
now = Time.now.to_i
|
65
|
+
if @time != now
|
66
|
+
log '--'
|
67
|
+
lines = stats
|
68
|
+
lines.sort! {|a,b| a[1]==b[1] ? a[2]<=>b[2] : a[1]<=>b[1] }
|
69
|
+
lines.each do |vars|
|
70
|
+
vars << bargraph(vars[2], 100 * @cores)
|
71
|
+
log "%#{@longest[:name]}s %#{@longest[:pid]}d CPU: %6.2f%% [%-40s]" % vars
|
72
|
+
end
|
73
|
+
end
|
74
|
+
sleep 0.1
|
75
|
+
@time = now
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def bargraph(value, max=100, length=40, on='#', off='.')
|
80
|
+
(on * (([[value, 0].max, max].min * length) / max).to_i).ljust(length, off)
|
81
|
+
end
|
82
|
+
|
83
|
+
def log(message)
|
84
|
+
msg = "#{Time.now.strftime('%F %H:%M:%S')} TourWatch: #{message}"
|
85
|
+
puts msg
|
86
|
+
File.open(@logfile, "a") {|f| f.puts msg } if @logfile
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'mechanize', ">= 0.7.6"
|
3
|
+
gem "hpricot", ">= 0.6"
|
4
|
+
$: << File.join(File.dirname(__FILE__), 'lib')
|
5
|
+
|
6
|
+
require 'hpricot'
|
7
|
+
require 'mechanize'
|
8
|
+
|
9
|
+
WWW::Mechanize.html_parser = Hpricot
|
10
|
+
|
11
|
+
require 'web_sickle'
|
12
|
+
require "assertions"
|
13
|
+
require "hash_proxy"
|
14
|
+
require "helpers/asp_net"
|
15
|
+
require "helpers/table_reader"
|
16
|
+
|
17
|
+
Hpricot.buffer_size = 524288
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class WebSickleAssertionException < Exception; end
|
2
|
+
|
3
|
+
module WebSickle::Assertions
|
4
|
+
def assert_equals(expected, actual, message = nil)
|
5
|
+
unless(expected == actual)
|
6
|
+
report_error <<-EOF
|
7
|
+
Error: Expected
|
8
|
+
#{expected.inspect}, but got
|
9
|
+
#{actual.inspect}
|
10
|
+
#{message}
|
11
|
+
EOF
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def assert_select(selector, message)
|
16
|
+
assert_select_in(@page, selector, message)
|
17
|
+
end
|
18
|
+
|
19
|
+
def assert_no_select(selector, message)
|
20
|
+
assert_no_select_in(@page, selector, message)
|
21
|
+
end
|
22
|
+
|
23
|
+
def assert_select_in(content, selector, message)
|
24
|
+
report_error("Error: Expected selector #{selector.inspect} to find a page element, but didn't. #{message}") if (content / selector).blank?
|
25
|
+
end
|
26
|
+
|
27
|
+
def assert_no_select_in(content, selector, message)
|
28
|
+
report_error("Error: Expected selector #{selector.inspect} to not find a page element, but did. #{message}") unless (content / selector).blank?
|
29
|
+
end
|
30
|
+
|
31
|
+
def assert_contains(left, right, message = nil)
|
32
|
+
(right.is_a?(Array) ? right : [right]).each do | item |
|
33
|
+
report_error("Error: Expected #{left.inspect} to contain #{right.inspect}, but didn't. #{message}") unless left.include?(item)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def assert(passes, message = nil)
|
38
|
+
report_error("Error: expected true, got false. #{message}") unless passes
|
39
|
+
end
|
40
|
+
|
41
|
+
def assert_link_text(link, text)
|
42
|
+
case text
|
43
|
+
when String
|
44
|
+
assert_equals(link.text, text)
|
45
|
+
when Regexp
|
46
|
+
assert(link.text.match(text))
|
47
|
+
else
|
48
|
+
raise ArgumentError, "Don't know how to assert an object like #{text.inspect} - expected: Regexp or String"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module WebSickle::Helpers
|
2
|
+
module AspNet
|
3
|
+
def asp_net_do_postback(options)
|
4
|
+
target_element = case
|
5
|
+
when options[:button]
|
6
|
+
find_button(options[:button])
|
7
|
+
when options[:field]
|
8
|
+
find_field(options[:field])
|
9
|
+
else
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
@form.fields << WWW::Mechanize::Form::Field.new("__EVENTTARGET", target_element ? target_element.name : "") if target_element
|
13
|
+
submit_form_button
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module WebSickle::Helpers
|
2
|
+
class TableReader
|
3
|
+
attr_reader :headers, :options, :body_rows, :header_row, :extra_rows
|
4
|
+
|
5
|
+
def initialize(element, p_options = {})
|
6
|
+
@options = {
|
7
|
+
:row_selectors => [" > tr", "thead > tr", "tbody > tr"],
|
8
|
+
:header_selector => " > th",
|
9
|
+
:header_proc => lambda { |th| th.inner_text.gsub(/[\n\s]+/, ' ').strip },
|
10
|
+
:body_selector => " > td",
|
11
|
+
:body_proc => lambda { |header, td| td.inner_text.strip },
|
12
|
+
:header_offset => 0,
|
13
|
+
:body_offset => 1
|
14
|
+
}.merge(p_options)
|
15
|
+
@options[:body_range] ||= options[:body_offset]..-1
|
16
|
+
raw_rows = options[:row_selectors].map{|row_selector| element / row_selector}.compact.flatten
|
17
|
+
|
18
|
+
@header_row = raw_rows[options[:header_offset]]
|
19
|
+
@body_rows = raw_rows[options[:body_range]]
|
20
|
+
@extra_rows = (options[:body_range].last+1)==0 ? [] : raw_rows[(options[:body_range].last+1)..-1]
|
21
|
+
|
22
|
+
@headers = (@header_row / options[:header_selector]).map(&options[:header_proc])
|
23
|
+
end
|
24
|
+
|
25
|
+
def rows
|
26
|
+
@rows ||= @body_rows.map do |row|
|
27
|
+
hash = {}
|
28
|
+
data_array = (headers).zip(row / options[:body_selector]).each do |column_name, td|
|
29
|
+
hash[column_name] = options[:body_proc].call(column_name, td)
|
30
|
+
end
|
31
|
+
hash
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def array_to_hash(data, column_names)
|
36
|
+
column_names.inject({}) {|h,column_name| h[column_name] = data[column_names.index(column_name)]; h }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Nokogiri::XML::Element.class_eval do
|
2
|
+
# def inspect(indent = "")
|
3
|
+
# breaker = "\n#{indent}"
|
4
|
+
# if children.length == 0
|
5
|
+
# %(#{indent}<#{name}#{breaker} #{attributes.map {|k,v| k + '=' + v.inspect} * "#{breaker} "}/>)
|
6
|
+
# else
|
7
|
+
# %(#{indent}<#{name} #{attributes.map {|k,v| k + '=' + v.inspect} * " "}>\n#{children.map {|c| c.inspect(indent + ' ') rescue c.class} * "\n"}#{breaker}</#{name}>)
|
8
|
+
# end
|
9
|
+
# end
|
10
|
+
# end
|
11
|
+
# Nokogiri::XML::Text.class_eval do
|
12
|
+
# def inspect(indent = "")
|
13
|
+
# "#{indent}#{text.inspect}"
|
14
|
+
# end
|
15
|
+
# end
|