tourbus 0.1.2

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.
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
+