tourbus 0.1.2
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 +206 -0
- data/bin/tourbus +46 -0
- data/bin/tourwatch +52 -0
- data/examples/contact_app/README.rdoc +128 -0
- data/examples/contact_app/contact_app.rb +36 -0
- data/examples/contact_app/tours/simple.rb +19 -0
- data/examples/contact_app/tours/tourbus.yml +2 -0
- data/lib/common.rb +30 -0
- data/lib/runner.rb +80 -0
- data/lib/tour.rb +151 -0
- data/lib/tour_bus.rb +92 -0
- data/lib/tour_watch.rb +88 -0
- metadata +127 -0
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
|
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
|
+
|