tourbus 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2008-2009 David Brady github@shinybit.com
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,206 @@
1
+ = TourBus
2
+
3
+ Flexible and scalable website testing tool.
4
+
5
+ == Authors
6
+
7
+ * David Brady -- github@shinybit.com
8
+ * Tim Harper -- tim.harper@leadmediapartners.com
9
+ * James Britt -- james@neurogami.com
10
+ * JT Zemp -- jtzemp@gmail.com
11
+
12
+
13
+ == General Info
14
+
15
+ TourBus is an intelligent website load testing tool. Allows for
16
+ complicated testing scenarios including filling out forms, following
17
+ redirects, handling cookies, and following links--all of the things
18
+ you'd normally associate with a regression suite or integration
19
+ testing tool. The difference is that TourBus also scales concurrently,
20
+ and you can perform hundreds of complicated regression tests
21
+ simultaneously in order to thoroughly load test your website.
22
+
23
+ It uses Webrat::Mechanize to run the browsing session, so you get the
24
+ load testing you want, and all the sweetness of Webrat to write your
25
+ tests in.
26
+
27
+ == Motivation
28
+
29
+ I started writing TourBus because I needed flexibility and scalability
30
+ in a website testing tool, and the extant tools all provided one but
31
+ not the other. Selenium is ultraflexible but limited to the number of
32
+ browsers you can have open at once, while Apache Bench is powerful and
33
+ fast but limited to simple tests.
34
+
35
+ TourBus lets you define complicated paths through your website, then
36
+ execute those paths concurrently for stress testing.
37
+
38
+ == Example
39
+
40
+ To see TourBus in action, you need to write scripts. For lack of a
41
+ better name, these are called Tours.
42
+
43
+ === Example Tour
44
+
45
+ * Make a folder called tours and put a file in it called simple.rb. In
46
+ it write:
47
+
48
+ class Simple < Tour
49
+ def test_homepage
50
+ visit "http://#{@host}/"
51
+ assert_contain "My Home Page"
52
+ end
53
+ end
54
+
55
+ * Files in ./tours should have classes that match their names. E.g.
56
+ "class BigHairyTest < Tour" belongs in ./tours/big_hairy_test.rb
57
+
58
+ * Think Test::Unit. test_* methods will be found automagically.
59
+ setup() and teardown() methods will be executed at the appropriate
60
+ times.
61
+
62
+ === Example TourBus Run
63
+
64
+ You want to invoke +tourbus+ from the parent directory of the @tours/@ folder.
65
+
66
+ For example, if you have this project tree ...
67
+
68
+ `-- contact_app
69
+ |-- README.rdoc
70
+ |-- contact_app.rb
71
+ `-- tours
72
+ |-- simple.rb
73
+ `-- tourbus.yml
74
+
75
+ ... then you execute +tourbus+ from the +contact_app/+ directory.
76
+
77
+ tourbus -c 2 -n 3 simple
78
+
79
+ That will run the +simple.rb+ tour file.
80
+
81
+ It will create 2 concurrent Tour runners, each of which will run all
82
+ of the methods in Simple three times.
83
+
84
+ * You can specify multiple tours.
85
+
86
+ tourbus -c 2 -n 3 simple1 simple2 simple3
87
+
88
+ * If you don't specify a tour, all tours in ./tours will be run.
89
+
90
+ * tourbus --help will give you more information.
91
+
92
+ * You can run tours and filter given tests.
93
+
94
+ tourbus -c 2 -n 3 simple -t test_login,test_logout
95
+
96
+ Note that if you specify multiple tours and filter tests, the filtered
97
+ tests will be run on all tours specified. If you do not specify a
98
+ tour, the filtered tests will be run on all tours found in the
99
+ +./tours+ folder.
100
+
101
+ === Example TourWatch Run
102
+
103
+ On the webserver, you can type
104
+
105
+ tourwatch -c 4
106
+
107
+ To begin running tourwatch. It's basically a stripped-down version of
108
+ top with cheesy text graphs. (TourWatch's development cycles were
109
+ included in the 2 days for TourBus.)
110
+
111
+ * The -c option is for the total number of cores on the server. The
112
+ top app will cheerfully report a process as taking 392% CPU if it is
113
+ using 98% of four cores. This option is only necessary for making
114
+ the little text graphs scale correctly.
115
+
116
+ * You can choose which processes to watch by passing a csv to -p:
117
+
118
+ tourwatch -p ruby,mongrel
119
+
120
+ Each process name is a partial regexp, so the above would match
121
+ mongrel AND mongrel_rails, etc.
122
+
123
+ * tourwatch --help will give you more information.
124
+
125
+ == History and Status
126
+
127
+ TourBus began life as a 2-day throwaway app. It is definitely an app
128
+ whose development provides many opportunities for open-source
129
+ contributors to make improvements. It is chock-full of brutal hacks,
130
+ duplications, oversights, and kludges.
131
+
132
+ == Hacks, Kludges, Known Issues, and Piles of Steaming Poo
133
+
134
+ * If you give a tour a name that is pluralized, it won't work. This is
135
+ probably a bug worth fixing. The reason for it is that we take file
136
+ names and "classify" them, and e.g. "ranking_reports" becomes
137
+ "RankingReport", not "RankingReports". This is an artifact of
138
+ borrowing from Rails' activesupport libs and should probably be
139
+ fixed.
140
+
141
+ * Mechanize 0.8 doesn't always play well together with TourBus. If you
142
+ get "connection refused" socket errors, try upgrading to Mechanize
143
+ 0.9.
144
+
145
+ * JRuby doesn't play well with Nokogiri. I have set the html_parser to
146
+ use hpricot, which should work around the issue for now.
147
+
148
+ * There are no specs. Yikes! This is to my eternal shame because I'm
149
+ sort of a testing freak. Because TourBus *WAS* a testing tool, I
150
+ didn't put tests on it. I haven't put tests on it yet because I'm
151
+ not sure how to go about it. Instead of exercising a web app with a
152
+ test browser, we need to exercise a test browser with... um... a web
153
+ app? (dbrady notes: Now that we have a contact_app, we could try
154
+ writing some specs and features to run tourbus against it.)
155
+
156
+ * Web-Sickle is another internal app, written by Tim Harper, that
157
+ works "well enough". Until I open-sourced this project, it was a
158
+ submodule in the app. We wanted to keep TourBus extensions separate
159
+ from WebSickle itself, so there's a lot of code in Runner that
160
+ really belongs in WebSickle.
161
+
162
+ * Documentation is <strike>horrible</strike> merely quite bad.
163
+
164
+ * There's not much in the way of examples, either. When I removed all
165
+ the LMP-specific code, all of the examples went with it. Sorry about
166
+ that. Coming soon.
167
+
168
+ == Feature Requests (How You Can Help!)
169
+
170
+ * I'd like to beef up the example contact app to show more of TourBus
171
+ than the simplest possible path. Adding in another page or two, and
172
+ then adding an additional tour or two would make it more apparent to
173
+ new users that you can do things like run multiple tours at once.
174
+ Also, having more than one test_ method in simple.rb would let us
175
+ demonstrate test filtering as well. (Be aware that at present
176
+ [2009-04-17], webrat still does not play well with Sinatra sessions
177
+ so there would be complications. dbrady's fork of webrat combines
178
+ jferris' fix with the latest webrat, and will be maintained until
179
+ main webrat includes the feature. That fork is at
180
+ http://github.com/dbrady/webrat)
181
+
182
+ * I'd like to remove WebSickle and replace it with Webrat. There is a
183
+ webrat branch on the main fork (http://github.com/dbrady/tourbus)
184
+ that is 90% complete. Once that's done we can start massaging the
185
+ API to be a little more friendly. [done (but now that it is, it
186
+ needs a refactoring--Tour should probably inherit from
187
+ Webrat::Mechanize, not delegate to it.)]
188
+
189
+ == Credits
190
+
191
+ * Tim Harper camped at my place for a day fixing bugs in WebSickle as
192
+ I exercised more and more new bits of it. Thanks, dude.
193
+
194
+ * Lead Media Partners paid me to write TourBus, then let me open
195
+ source it. How much do they rock? All the way to 11, that's how much
196
+ they rock.
197
+
198
+ * James Britt jumped on this and revived it as it was gathering dust.
199
+ Thanks!
200
+
201
+ * JT Zemp added before_tour, after_tour. Thanks!
202
+
203
+ == License
204
+
205
+ MIT. See the license file.
206
+
data/bin/tourbus ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'common'))
3
+ require 'trollop'
4
+ require_all_files_in_folder 'tours'
5
+
6
+ # load config file, we'll use these as defaults
7
+ config_file = ["./tourbus.yml", "./tours/tourbus.yml", "./config/tourbus.yml", "~/tourbus.yml"].map {|p| File.expand_path(p)}.find {|p| File.exists? p}
8
+ config = config_file ? YAML::load_file(config_file).symbolize_keys : {}
9
+
10
+ config_map = { :host => :to_s, :concurrency => :to_i, :number => :to_i, :rand => :to_i, :tests => :to_s }
11
+ config_map.each {|key,conv| config[key] = config[key].send(conv) if config.key? key }
12
+
13
+ # defaults
14
+ config[:host] ||= "http://localhost:3000"
15
+ config[:concurrency] ||= 1
16
+ config[:number] ||= 1
17
+ config[:rand] ||= nil
18
+
19
+ opts = Trollop.options do
20
+ opt :host, "Remote hostname to test", :default => config[:host]
21
+ opt :concurrency, "Number of simultaneous runs to perform", :type => :integer, :default => config[:concurrency]
22
+ opt :number, "Number of times to run the tour (in each concurrent step, so -c 10 -n 10 will run the tour 100 times)", :type => :integer, :default => config[:number]
23
+ opt :list, "List tours and runs available. If tours or runs are included, filters the list", :type => :boolean, :default => nil
24
+ opt :rand, "Random seed", :type => :integer, :default => config[:rand]
25
+ opt :tests, "Test name(s) filter. The name of the test to run (use --list to see the test names). Use commas, no spaces, for mulitple names", :type => :string, :default => nil
26
+ end
27
+
28
+ tours = if ARGV.empty?
29
+ Tour.tours
30
+ else
31
+ ARGV
32
+ end
33
+
34
+ srand opts[:rand] || Time.now.to_i
35
+
36
+ if opts[:list]
37
+ Tour.tours(ARGV).each do |tour|
38
+ puts tour
39
+ puts Tour.tests(tour).map {|test| " #{test}"}
40
+ end
41
+ else
42
+ opts[:tests] = opts[:tests].split(',') if opts[:tests]
43
+
44
+ TourBus.new(opts[:host], opts[:concurrency], opts[:number], tours, opts[:tests]).run
45
+ end
46
+
data/bin/tourwatch ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # tourwatch - cheap monitor program for tourbus
4
+ #
5
+ # Notes:
6
+ #
7
+ # tourwatch is a cheap logger program for tourbus. It runs on the
8
+ # targeted server and monitors cpu and memory usage of webserver
9
+ # processes. It's a moderately quick hack: I have a 2-hour budget to
10
+ # write and debug the whole thing and here I am wasting time by
11
+ # starting with documentation. This is because I figure the chance of
12
+ # this program needing maintenance in the next 6 months to be well
13
+ # over 100%, and the poor guy behind me (Hey, that's you! Hi.) will
14
+ # need to know why tourwatch is so barebones.
15
+ #
16
+ # So. TourWatch runs on the target server, collects top information
17
+ # every second, and logs it to file. End of story. "Automation" is
18
+ # handled by the meat cloud (Hey, that's you! Hi.) when the maintainer
19
+ # starts and stops the process manually. Report collection is handled
20
+ # by you reading the logfiles in a terminal. Report aggregation is
21
+ # handled by you aggregating the reports. Yes, there's a theme here.
22
+ #
23
+ # TODO:
24
+ #
25
+ # - Remote reporting? Send log events to main log server?
26
+ #
27
+ # - If we logged to a lightweight database like sqlite3, we could do
28
+ # some clever things like track individual pids and process groups.
29
+ # This would let us track, e.g., aggregate apache stress as well as
30
+ # rogue mongrels. I'm not doing this now because it will require
31
+ # writing something to read and parse the previous information. For
32
+ # now, we'll leave it up to the user (Hey, that's you! Hi.) to parse
33
+ # the logfiles.
34
+ #
35
+ # - Tweak output format. Currently it's crap. I don't think we need
36
+ # dynamic templating or anything, but it might be nice to improve
37
+ # the existing formats.
38
+
39
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'common'))
40
+ require 'trollop'
41
+ require 'tour_watch'
42
+
43
+ opts = Trollop.options do
44
+ opt :outfile, "Logfile name (default to STDOUT)", :type => :string, :default => nil
45
+ opt :processes, "csv of processes to monitor", :type => :string, :default => nil
46
+ opt :cores, "number of cores present (max CPU% is number of cores * 100)", :type => :integer, :default => 4
47
+ opt :mac, "Set if running on MacOSX. The Mac top command is different than linux top.", :type => :boolean, :default => false
48
+ end
49
+
50
+ TourWatch.new(opts).run
51
+
52
+
@@ -0,0 +1,128 @@
1
+ = Contact App
2
+
3
+ Silly little contact app to show you how to tour a website.
4
+
5
+ = Requirements
6
+
7
+ In addition to tourbus, you will need Sinatra to run
8
+ this app.
9
+
10
+ sudo gem install sinatra
11
+
12
+ = Contact App
13
+
14
+ == Start the app
15
+
16
+ Once that's working, start the app with "ruby contact_app.rb". Sinatra
17
+ should start up, and you can now point your browser at
18
+ http://localhost:4567 to see the app's homepage.
19
+
20
+ Pretty humble, I know; just the one link labeled Enter Contacts. Click
21
+ it to get to the Contact form. Here you can enter a first and last
22
+ name then click submit.
23
+
24
+ The app then shows you that name in last_name, first_name format.
25
+ That's the whole app. Don't everybody applaud all at once.
26
+
27
+ == First Tour
28
+
29
+ Still here? Okay, let's tour this website.
30
+
31
+ In the tours folder, you will find two files: simple.rb and
32
+ tourbus.yml. The YAML file just sets the default host to
33
+ localhost:4567. (Without it, tourbus will default to http://localhost:3000.
34
+ You could override this by running tourbus with "-h localhost:4567"
35
+ every time, but that gets tedious.
36
+
37
+ Before we go any farther, let's run tourbus. Leave Sinatra running and
38
+ open another terminal window. Go into the contact_app folder and just
39
+ type "tourbus". You should get a screenful of information ending with
40
+ a happy little banner something like this:
41
+
42
+ 2009-01-10 12:09:36 TourBus: --------------------------------------------------------------------------------
43
+ 2009-01-10 12:09:36 TourBus: 1 runs: 1x1 of simple
44
+ 2009-01-10 12:09:36 TourBus: All Runners finished.
45
+ 2009-01-10 12:09:36 TourBus: Total Runs: 1
46
+ 2009-01-10 12:09:36 TourBus: Total Passes: 1
47
+ 2009-01-10 12:09:36 TourBus: Total Fails: 0
48
+ 2009-01-10 12:09:36 TourBus: Total Errors: 0
49
+ 2009-01-10 12:09:36 TourBus: Elapsed Time: 0.0131220817565918
50
+ 2009-01-10 12:09:36 TourBus: Speed: 76.207 v/s
51
+ 2009-01-10 12:09:36 TourBus: --------------------------------------------------------------------------------
52
+
53
+ == Tourbus Defaults
54
+
55
+ Tourbus tries to be sensible; if you don't provide a number of runs or
56
+ concurrency, it sets them to 1. If you don't choose a tour to run, it
57
+ runs them all. It looks for tourbus.yml in the current folder,
58
+ ./tours, in ./config (a Rails convention), and in your home folder.
59
+ (It looks for them in that order, and stops as soon as it finds one.
60
+ It does not merge multiple yaml files together.)
61
+
62
+ == Simple Tour
63
+
64
+ Okay, now let's look at tours/simple.rb.
65
+
66
+ It defines a class named Simple that inherits from Tour. Tourbus won't
67
+ try to run a tour unless the file contains a Tour child class of the
68
+ same name as the file.
69
+
70
+ Inside the class, methods whose names begin with test_ will
71
+ automatically be run as part of the tour. They are not run in any
72
+ particular order.
73
+
74
+ === test_home
75
+
76
+ Right. Let's look test_home first, because it's simpler:
77
+
78
+ def test_home
79
+ visit "/"
80
+ assert_contain "If you click this"
81
+
82
+ click_link "Enter Contact"
83
+ assert_match /\/contacts/, current_page.url
84
+ end
85
+
86
+ +visit+ is a webrat method that you can call inside of your tours. It opens the given path on the
87
+ host that tourbus is testing.
88
+
89
+ +assert_contain+ is also a webrat method that confirms the given string is on the page.
90
+
91
+ +click_link+ does what you'd expect. It takes a hash that identifies
92
+ the link to click. +click_link+ will raise an exception
93
+ if it cannot find the link to click.
94
+
95
+ +assert_match+ comes from Test::Unit which is used internally to webrat. It will raise an exception unless the uri
96
+ matches the given regexp.
97
+
98
+ So you should be able to use any Webrat locator or matcher, and any of the Test::Unit assertions.
99
+
100
+ === test_contacts
101
+
102
+ Okay, let's actually submit a form.
103
+
104
+ def test_contacts
105
+ visit "/contacts"
106
+
107
+ fill_in "first_name", :with => "Joe"
108
+ fill_in "last_name", :with => "Tester"
109
+ click_button
110
+
111
+ assert_contain "Tester, Joe"
112
+ end
113
+
114
+ test_contacts starts by going directly to the contacts app. Note that
115
+ the leading "/" isn't optional.
116
+
117
+ +fill_in+ is a Webrat method that will look for form fields based on ids, label text, and other things.
118
+ It's matchers are pretty good. Check out Webrat's documentation for more info. In the examples above,
119
+ we're finding the fields for the first name and last name and putting in "Joe" and "Tester" respectively.
120
+ +fill_in+ asserts that the fields actually exist and will raise an exception if they don't.
121
+
122
+ +click_button+ does what its name implies. It finds the correct form to
123
+ submit Webrat is smart like that. *Note:* Like +click_link+, +click_button+
124
+ contains some implicit assertions and will raise an exception if the button doesn't exist.
125
+
126
+ +assert_contain+ we've already seen.
127
+
128
+ Good luck, and happy touring!
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ # Contact app. Example Sinatra application that you can use to test tourbus.
4
+ #
5
+ # Pretty simple applet. You go to / and enter your contact
6
+ # information. When you click submit, it shows you your name in all
7
+ # caps. Okay, "pretty simple" was an understatement. I get that. Shut up.
8
+ require 'rubygems'
9
+ require 'sinatra'
10
+
11
+ get '/' do
12
+ %{If you click this, I'll take you to a page where you can enter your contact info: <a href="/contacts">Enter Contact</a>}
13
+ end
14
+
15
+ get '/contacts' do
16
+ <<-eos
17
+ <html>
18
+ <head>
19
+ <title>Contact App</title>
20
+ </head>
21
+ <body>
22
+ <h1>Contact Info:</h1>
23
+ <form action="/contacts" method="POST">
24
+ <p><label for="first_name"><b>First Name:</b></label> <input name="first_name" size="30"></p>
25
+ <p><label for="last_name"><b>Last Name:</b></label> <input name="last_name" size="30"></p>
26
+ <input type="submit" value="Submit">
27
+ </form>
28
+ </body>
29
+ </html>
30
+ eos
31
+ end
32
+
33
+ post '/contacts' do
34
+ "<h1>#{params[:last_name]}, #{params[:first_name]}</h1>"
35
+ end
36
+
@@ -0,0 +1,19 @@
1
+ class Simple < Tour
2
+ def test_home
3
+ visit "/"
4
+ assert_contain "If you click this"
5
+
6
+ click_link "Enter Contact"
7
+ assert_match /\/contacts/, current_page.url
8
+ end
9
+
10
+ def test_contacts
11
+ visit "/contacts"
12
+
13
+ fill_in "first_name", :with => "Joe"
14
+ fill_in "last_name", :with => "Tester"
15
+ click_button
16
+
17
+ assert_contain "Tester, Joe"
18
+ end
19
+ end
@@ -0,0 +1,2 @@
1
+ ---
2
+ host: http://localhost:4567
data/lib/common.rb ADDED
@@ -0,0 +1,30 @@
1
+ # common.rb - Common settings, requires and helpers
2
+ unless defined? TOURBUS_LIB_PATH
3
+ TOURBUS_LIB_PATH = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ $:<< TOURBUS_LIB_PATH unless $:.include? TOURBUS_LIB_PATH
5
+ end
6
+
7
+ require 'rubygems'
8
+
9
+ gem 'mechanize', ">= 0.8.5"
10
+ gem 'trollop', ">= 1.10.0"
11
+ gem 'faker', '>= 0.3.1'
12
+
13
+ # TODO: I'd like to remove dependency on Rails. Need to see what all
14
+ # we're using (like classify) and remove each dependency individually.
15
+ require 'activesupport'
16
+
17
+ require 'monitor'
18
+ require 'faker'
19
+ require 'tour_bus'
20
+ require 'runner'
21
+ require 'tour'
22
+
23
+ class TourBusException < Exception; end
24
+
25
+ def require_all_files_in_folder(folder, extension = "*.rb")
26
+ for file in Dir[File.join('.', folder, "**/#{extension}")]
27
+ require file
28
+ end
29
+ end
30
+
data/lib/runner.rb ADDED
@@ -0,0 +1,80 @@
1
+ require 'monitor'
2
+ require 'common'
3
+
4
+ # The common base class for all exceptions raised by Webrat.
5
+ class WebratError < StandardError ; end
6
+
7
+ class Runner
8
+ attr_reader :host, :tours, :number, :runner_type, :runner_id
9
+
10
+ def initialize(host, tours, number, runner_id, test_list)
11
+ @host, @tours, @number, @runner_id, @test_list = host, tours, number, runner_id, test_list
12
+ @runner_type = self.send(:class).to_s
13
+ log("Ready to run #{@runner_type}")
14
+ end
15
+
16
+ # Dispatches to subclass run method
17
+ def run_tours
18
+ log "Filtering on tests #{@test_list.join(', ')}" unless @test_list.to_a.empty?
19
+ tours,tests,passes,fails,errors = 0,0,0,0,0
20
+ 1.upto(number) do |num|
21
+ log("Starting #{@runner_type} run #{num}/#{number}")
22
+ @tours.each do |tour_name|
23
+
24
+ log("Starting run #{number} of Tour #{tour_name}")
25
+ tours += 1
26
+ tour = Tour.make_tour(tour_name,@host,@tours,@number,@runner_id)
27
+ tour.before_tour
28
+
29
+ tour.tests.each do |test|
30
+ times = Hash.new {|h,k| h[k] = {}}
31
+
32
+ next if test_limited_to(test) # test_list && !test_list.empty? && !test_list.include?(test.to_s)
33
+
34
+ begin
35
+ tests += 1
36
+ times[test][:started] = Time.now
37
+ tour.run_test test
38
+ passes += 1
39
+ rescue TourBusException, WebratError => e
40
+ log("********** FAILURE IN RUN! **********")
41
+ log e.message
42
+ e.backtrace.each do |trace|
43
+ log trace
44
+ end
45
+ fails += 1
46
+ rescue Exception => e
47
+ log("*************************************")
48
+ log("*********** ERROR IN RUN! ***********")
49
+ log("*************************************")
50
+ log e.message
51
+ e.backtrace.each do |trace|
52
+ log trace
53
+ end
54
+ errors += 1
55
+ ensure
56
+ times[test][:finished] = Time.now
57
+ times[test][:elapsed] = times[test][:finished] - times[test][:started]
58
+ end
59
+ log("Finished run #{number} of Tour #{tour_name}")
60
+ end
61
+
62
+ tour.after_tour
63
+ end
64
+ log("Finished #{@runner_type} run #{num}/#{number}")
65
+ end
66
+ log("Finished all #{@runner_type} tours.")
67
+ [tours,tests,passes,fails,errors]
68
+ end
69
+
70
+ protected
71
+
72
+ def log(message)
73
+ puts "#{Time.now.strftime('%F %H:%M:%S')} Runner ##{@runner_id}: #{message}"
74
+ end
75
+
76
+ def test_limited_to(test_name)
77
+ @test_list && !@test_list.empty? && !@test_list.include?(test_name.to_s)
78
+ end
79
+ end
80
+
data/lib/tour.rb ADDED
@@ -0,0 +1,151 @@
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
+ :attach_file,
25
+ :attaches_file,
26
+ :automate,
27
+ :basic_auth,
28
+ :check,
29
+ :check_for_infinite_redirects,
30
+ :checks,
31
+ :choose,
32
+ :chooses,
33
+ :click_area,
34
+ :click_button,
35
+ :click_link,
36
+ :click_link_within,
37
+ :clicks_area,
38
+ :clicks_button,
39
+ :clicks_link,
40
+ :current_page,
41
+ :dom,
42
+ :field_by_xpath,
43
+ :field_labeled,
44
+ :field_with_id,
45
+ :fill_in,
46
+ :fills_in,
47
+ :get,
48
+ :header,
49
+ :http_accept,
50
+ :infinite_redirect_limit_exceeded?,
51
+ :internal_redirect?,
52
+ :redirected_to,
53
+ :reload,
54
+ :response_body,
55
+ :select,
56
+ :select_date,
57
+ :select_datetime,
58
+ :select_option,
59
+ :select_time,
60
+ :selects,
61
+ :selects_date,
62
+ :selects_datetime,
63
+ :selects_time,
64
+ :set_hidden_field,
65
+ :simulate,
66
+ :submit_form,
67
+ :uncheck,
68
+ :unchecks,
69
+ :within,
70
+ :xml_content_type?
71
+ ].each {|m| def_delegators(:webrat_session, m) }
72
+
73
+ def initialize(host, tours, number, tour_id)
74
+ @host, @tours, @number, @tour_id = host, tours, number, tour_id
75
+ @tour_type = self.send(:class).to_s
76
+ @webrat_session = Webrat::MechanizeAdapter.new()
77
+ end
78
+
79
+ def visit(url, data=nil)
80
+ get url, data
81
+ end
82
+
83
+ # before_tour runs once per tour, before any tests get run
84
+ def before_tour; end
85
+
86
+ # after_tour runs once per tour, after all the tests have run
87
+ def after_tour; end
88
+
89
+ def setup
90
+ end
91
+
92
+ def teardown
93
+ end
94
+
95
+ def wait(time)
96
+ sleep time.to_i
97
+ end
98
+
99
+ # Lists tours in tours folder. If a string is given, filters the
100
+ # list by that string. If an array of filter strings is given,
101
+ # returns items that match ANY filter string in the array.
102
+ def self.tours(filter=[])
103
+ filter = [filter].flatten
104
+ # All files in tours folder, stripped to basename, that match any item in filter
105
+ # I do loves me a long chain. This returns an array containing
106
+ # 1. All *.rb files in tour folder (recursive)
107
+ # 2. Each filename stripped to its basename
108
+ # 3. If you passed in any filters, these basenames are rejected unless they match at least one filter
109
+ # 4. The filenames remaining are then checked to see if they define a class of the same name that inherits from Tour
110
+ 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 }
111
+ end
112
+
113
+ def self.tests(tour_name)
114
+ Tour.make_tour(tour_name).tests
115
+ end
116
+
117
+ def self.tour?(tour_name)
118
+ Object.const_defined?(tour_name.classify) && tour_name.classify.constantize.ancestors.include?(Tour)
119
+ end
120
+
121
+ # Factory method, creates the named child class instance
122
+ def self.make_tour(tour_name,host="http://localhost:3000",tours=[],number=1,tour_id=nil)
123
+ tour_name.classify.constantize.new(host,tours,number,tour_id)
124
+ end
125
+
126
+ # Returns list of tests in this tour. (Meant to be run on a subclass
127
+ # instance; returns the list of tests available).
128
+ def tests
129
+ methods.grep(/^test_/).map {|m| m.sub(/^test_/,'')}
130
+ end
131
+
132
+ def run_test(test_name)
133
+ @test = "test_#{test_name}"
134
+ raise TourBusException.new("run_test couldn't run test '#{test_name}' because this tour did not respond to :#{@test}") unless respond_to? @test
135
+ setup
136
+ send @test
137
+ teardown
138
+ end
139
+
140
+ protected
141
+
142
+ def session
143
+ @session ||= Webrat::MechanizeSession.new
144
+ end
145
+
146
+ def log(message)
147
+ puts "#{Time.now.strftime('%F %H:%M:%S')} Tour ##{@tour_id}: (#{@test}) #{message}"
148
+ end
149
+
150
+ end
151
+
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
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tourbus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - David Brady
8
+ - James Britt
9
+ - JT Zemp
10
+ - Tim Harper
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+
15
+ date: 2009-11-22 00:00:00 -07:00
16
+ default_executable:
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: mechanize
20
+ type: :runtime
21
+ version_requirement:
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.8.5
27
+ version:
28
+ - !ruby/object:Gem::Dependency
29
+ name: trollop
30
+ type: :runtime
31
+ version_requirement:
32
+ version_requirements: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: "0"
37
+ version:
38
+ - !ruby/object:Gem::Dependency
39
+ name: faker
40
+ type: :runtime
41
+ version_requirement:
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ version:
48
+ - !ruby/object:Gem::Dependency
49
+ name: hpricot
50
+ type: :runtime
51
+ version_requirement:
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ - !ruby/object:Gem::Dependency
59
+ name: webrat
60
+ type: :runtime
61
+ version_requirement:
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ description: TourBus, a web stress-testing tool that combines complex 'tour' definitions with scalable concurrent testing
69
+ email: github@shinybit.com
70
+ executables:
71
+ - tourbus
72
+ - tourwatch
73
+ extensions: []
74
+
75
+ extra_rdoc_files:
76
+ - README.rdoc
77
+ - MIT-LICENSE
78
+ - examples/contact_app/README.rdoc
79
+ files:
80
+ - bin/tourbus
81
+ - bin/tourwatch
82
+ - examples/contact_app/README.rdoc
83
+ - examples/contact_app/contact_app.rb
84
+ - examples/contact_app/tours/simple.rb
85
+ - examples/contact_app/tours/tourbus.yml
86
+ - lib/common.rb
87
+ - lib/runner.rb
88
+ - lib/tour.rb
89
+ - lib/tour_bus.rb
90
+ - lib/tour_watch.rb
91
+ - README.rdoc
92
+ - MIT-LICENSE
93
+ has_rdoc: true
94
+ homepage: http://github.com/dbrady/tourbus/
95
+ licenses: []
96
+
97
+ post_install_message:
98
+ rdoc_options:
99
+ - --line-numbers
100
+ - --inline-source
101
+ - --main
102
+ - README.rdoc
103
+ - --title
104
+ - Tourbus - Web Stress Testing in Ruby
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: "0"
112
+ version:
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: "0"
118
+ version:
119
+ requirements: []
120
+
121
+ rubyforge_project:
122
+ rubygems_version: 1.3.5
123
+ signing_key:
124
+ specification_version: 3
125
+ summary: TourBus web stress-testing tool
126
+ test_files: []
127
+