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