bane 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +30 -30
- data/Rakefile +31 -40
- data/TODO +18 -10
- data/bin/bane +15 -10
- data/examples/multiple_behaviors.rb +24 -0
- data/examples/readme_example.rb +9 -0
- data/examples/single_behavior.rb +18 -0
- data/lib/bane.rb +2 -2
- data/lib/bane/{delegating_gserver.rb → behavior_server.rb} +11 -6
- data/lib/bane/behaviors.rb +29 -20
- data/lib/bane/command_line_configuration.rb +96 -0
- data/lib/bane/configuration_parser.rb +19 -4
- data/lib/bane/launcher.rb +7 -8
- data/lib/bane/service_registry.rb +4 -0
- data/test/bane/{delegating_gserver_test.rb → behavior_server_test.rb} +19 -16
- data/test/bane/behaviors_test.rb +32 -36
- data/test/bane/command_line_configuration_test.rb +100 -0
- data/test/bane/configuration_parser_test.rb +42 -55
- data/test/bane/fake_connection_test.rb +1 -1
- data/test/bane/integration_test.rb +45 -20
- data/test/bane/naive_http_response_test.rb +1 -1
- data/test/bane/service_registry_test.rb +1 -1
- metadata +63 -44
- data/examples/simple_port_and_class_as_constant.rb +0 -9
- data/examples/simple_port_and_class_as_string.rb +0 -7
- data/examples/specify_behavior_options.rb +0 -16
- data/examples/specify_ports.rb +0 -15
- data/lib/bane/configuration.rb +0 -50
- data/test/bane/configuration_test.rb +0 -52
- data/test/bane/launcher_test.rb +0 -16
data/README.md
CHANGED
@@ -2,11 +2,15 @@
|
|
2
2
|
|
3
3
|
Bane is a test harness used to test your application's interaction with other servers. It is based upon the material from Michael Nygard's ["Release It!"](http://www.pragprog.com/titles/mnee/release-it) book as described in the "Test Harness" chapter.
|
4
4
|
|
5
|
+
## Why Use Bane?
|
6
|
+
|
7
|
+
If you are building an application, you may depend on third-party servers or web services for your data. Most of the time these services are reliable, but at some point they will behave in an unusual manner - such as connecting but never respond, sending data very slowly, or sending an unexpected response. To ensure your application survives these scenarios, you should test your application against these bad behaviors. Bane helps you recreate these scenarios by standing in for your third-party servers and responding in several nefarious ways.
|
8
|
+
|
5
9
|
## Setup
|
6
10
|
|
7
11
|
Bane is available as a gem. Install it with
|
8
12
|
|
9
|
-
`
|
13
|
+
`gem install bane`
|
10
14
|
|
11
15
|
Note that Bane installs an executable, `bane`. Simply invoke `bane` with no arguments to get a usage description.
|
12
16
|
|
@@ -26,24 +30,37 @@ Bane is designed with a few usage scenarios in mind:
|
|
26
30
|
|
27
31
|
Example: `$ bane 3000`
|
28
32
|
|
29
|
-
4. Advanced Configuration using Ruby. If you want to modify some of the defaults used in the included behaviors, you can
|
33
|
+
4. Advanced Configuration using Ruby. If you want to modify some of the defaults used in the included behaviors, you can create a Ruby script to invoke Bane. For example, you might want to specify a response for the FixedResponse behavior:
|
30
34
|
|
31
35
|
Example:
|
32
36
|
|
33
|
-
|
37
|
+
```
|
38
|
+
require 'bane'
|
39
|
+
|
40
|
+
include Bane
|
41
|
+
|
42
|
+
launcher = Launcher.new(
|
43
|
+
[BehaviorServer.new(3000, Behaviors::FixedResponse.new(:message => "Shall we play a game?"))])
|
44
|
+
launcher.start
|
45
|
+
launcher.join
|
46
|
+
```
|
47
|
+
|
48
|
+
See the `examples`directory for more examples. For a list of options supported by the
|
49
|
+
included behaviors, see the source for the behaviors in `Bane::Behaviors` at `lib/bane/behaviors.rb`.
|
50
|
+
|
51
|
+
## Listening on all hosts
|
52
|
+
|
53
|
+
By default, Bane will listen only to connections on localhost (127.0.0.1).
|
34
54
|
|
35
|
-
|
36
|
-
include Behaviors
|
55
|
+
To listen on all hosts (0.0.0.0), start Bane from the command line with the `-a` or `--listen-on-all-hosts` option. For more command line help, run `bane -h` or `bane --help`.
|
37
56
|
|
38
|
-
|
39
|
-
3000 => {:behavior => FixedResponse, :message => "Shall we play a game?"},
|
40
|
-
)
|
41
|
-
)
|
42
|
-
launcher.start
|
43
|
-
launcher.join
|
57
|
+
## Keeping the Connection Open
|
44
58
|
|
45
|
-
|
46
|
-
|
59
|
+
By default, the socket behaviors that send any data will close the connection immediately after sending the response. There are variations of these behaviors available that end with `ForEachLine` which will wait for a line of input (using IO's `gets`), respond, then return to the waiting for input state.
|
60
|
+
|
61
|
+
For example, if you want to send a static response and then close the connection immediately, use `FixedResponse`. If you want to keep the connection open and respond to every line of input with the same data, use `FixedResponseForEachLine`. Note that these behaviors will never close the connection; they will happily respond to every line of input until you stop Bane.
|
62
|
+
|
63
|
+
If you are implementing a new behavior, you should consider whether or not you would like to provide another variation which keeps a connection open and responds after every line of input. If so, create the basic behavior which responds and closes the connection immediately, then create another behavior which includes the `ForEachLine` module. See the source in `lib/bane/behaviors.rb` for some examples.
|
47
64
|
|
48
65
|
## Background
|
49
66
|
|
@@ -76,20 +93,3 @@ TCP packets at a low level; which may require a C or C++ extension.
|
|
76
93
|
* The connection can be established, but packets could be lost causing retransmit delays
|
77
94
|
* The connection can be established, but the remote end never acknowledges receiving a packet, causing endless retransmits
|
78
95
|
|
79
|
-
## Design
|
80
|
-
|
81
|
-
Bane Behaviors are simple objects which implement the Strategy pattern. This makes them
|
82
|
-
simple to unit test and allows them to be independent of the underlying server implementation.
|
83
|
-
Bane currently serves all Behaviors using Ruby's built-in GServer, which provides a simple
|
84
|
-
multi-threaded TCP server. Behaviors currently:
|
85
|
-
|
86
|
-
* Accept an IO stream used to read from or send a response.
|
87
|
-
* Accept a hash of configuration options to allow overriding of default behavior parameters.
|
88
|
-
* Provide a meaningful name to appear in the Bane log. This is especially helpful if your application
|
89
|
-
under test dies and you'd like to identify which behavior killed it.
|
90
|
-
|
91
|
-
To enable support of different types of behaviors such as HTTP responses or low-level TCP packet
|
92
|
-
munging, a different base server instead of GServer may be required. In that case, it should
|
93
|
-
be possible to change how Behaviors are associated with a server, perhaps by making
|
94
|
-
Behaviors extend a server base class.
|
95
|
-
|
data/Rakefile
CHANGED
@@ -1,60 +1,51 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
2
12
|
require 'rake'
|
3
13
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
gem.add_development_dependency('mocha', '>= 0.9.8')
|
20
|
-
end
|
21
|
-
Jeweler::GemcutterTasks.new
|
22
|
-
rescue LoadError
|
23
|
-
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
gem.name = "bane"
|
17
|
+
gem.homepage = "http://github.com/danielwellman/bane"
|
18
|
+
gem.license = "BSD"
|
19
|
+
gem.summary = "A test harness for socket connections based upon ideas from Michael Nygard's 'Release It!'"
|
20
|
+
gem.description = <<-END
|
21
|
+
Bane is a test harness used to test your application's interaction with
|
22
|
+
other servers. It is based upon the material from Michael Nygard's "Release
|
23
|
+
It!" book as described in the "Test Harness" chapter.
|
24
|
+
END
|
25
|
+
gem.authors = ["Daniel Wellman"]
|
26
|
+
gem.email = "dan@danielwellman.com"
|
27
|
+
gem.files = FileList[ 'lib/**/*', 'bin/*', 'test/**/*', 'examples/*',
|
28
|
+
'Rakefile' ]
|
24
29
|
end
|
30
|
+
Jeweler::RubygemsDotOrgTasks.new
|
31
|
+
|
32
|
+
|
25
33
|
|
26
34
|
require 'rake/testtask'
|
27
|
-
|
28
|
-
Rake::TestTask.new do |test|
|
35
|
+
Rake::TestTask.new(:test) do |test|
|
29
36
|
test.libs << 'test'
|
30
37
|
test.test_files = FileList['test/**/*_test.rb']
|
31
38
|
test.verbose = true
|
32
39
|
end
|
33
40
|
|
34
|
-
begin
|
35
|
-
require 'rcov/rcovtask'
|
36
|
-
Rcov::RcovTask.new do |test|
|
37
|
-
test.libs << 'test'
|
38
|
-
test.pattern = 'test/**/*_test.rb'
|
39
|
-
test.verbose = true
|
40
|
-
end
|
41
|
-
rescue LoadError
|
42
|
-
task :rcov do
|
43
|
-
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
|
48
|
-
task :test => :check_dependencies
|
49
|
-
|
50
41
|
task :default => :test
|
51
42
|
|
52
|
-
require '
|
43
|
+
require 'rdoc/task'
|
53
44
|
Rake::RDocTask.new do |rdoc|
|
54
45
|
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
55
46
|
|
56
47
|
rdoc.rdoc_dir = 'rdoc'
|
57
|
-
rdoc.title = "
|
48
|
+
rdoc.title = "list #{version}"
|
58
49
|
rdoc.rdoc_files.include('README*')
|
59
50
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
60
51
|
end
|
data/TODO
CHANGED
@@ -1,13 +1,21 @@
|
|
1
|
-
|
2
|
-
-
|
3
|
-
|
4
|
-
|
5
|
-
-
|
6
|
-
|
7
|
-
|
1
|
+
Features:
|
2
|
+
- Quieter exits on Ctrl-C (than plain stack trace)
|
3
|
+
|
4
|
+
Design questions / Ideas:
|
5
|
+
- Make BasicBehavior a module called Behavior, and include that in each behavior?
|
6
|
+
- Is there a natural separation in Configuration between parsing arguments and instantiating/finding behaviors?
|
7
|
+
In particular, mocking "new" on a class seems to be a smell -- at least because I keep forgetting that 'new' must
|
8
|
+
return an object, and my mocks didn't do that (see CommandLineConfigurationTest, and the messier ConfigurationParserTest)
|
9
|
+
- Explore the ServiceRegistry usage -- should "find" behaviors be there? Printing?
|
10
|
+
- Decide if the current behaviors should be tied more directly to BehaviorServer, via subclassing or some other way.
|
11
|
+
- Figure out where the logger configuration logic belongs in the Launcher/BehaviorServer relationship
|
12
|
+
- Should the default logger go to STDERR or STDOUT?
|
13
|
+
- Break the Behaviors out into several files and test files?
|
8
14
|
- Log every request to the server/behavior, in addition to open, close. For this to work, it would have to be the
|
9
15
|
behavior's responsibility, since GServer#serve gets called only once for the lifetime of the connection.
|
10
16
|
|
11
|
-
|
12
|
-
-
|
13
|
-
|
17
|
+
Future Behaviors:
|
18
|
+
- Create a more configurable version of the DelugeResponse which allows for a header, footer, content and times to repeat.
|
19
|
+
- Write the remaining bad HTTP behaviors. In addition, we may want to replace the NaiveHttpResponse with something
|
20
|
+
from the standard Ruby library, so that there's less code in this project, and so we know that we're
|
21
|
+
following the HTTP protocol.
|
data/bin/bane
CHANGED
@@ -3,15 +3,20 @@
|
|
3
3
|
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
4
4
|
require 'bane'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
launcher = Bane::Launcher.new(Configuration(*ARGV))
|
14
|
-
launcher.start
|
15
|
-
launcher.join
|
6
|
+
config = Bane::CommandLineConfiguration.new()
|
7
|
+
begin
|
8
|
+
servers = config.parse(ARGV)
|
9
|
+
rescue Bane::ConfigurationError => ce
|
10
|
+
puts ce.message
|
11
|
+
puts config.usage
|
12
|
+
exit 1
|
16
13
|
end
|
17
14
|
|
15
|
+
if servers.empty?
|
16
|
+
puts config.usage
|
17
|
+
exit 0
|
18
|
+
end
|
19
|
+
|
20
|
+
launcher = Bane::Launcher.new(servers)
|
21
|
+
launcher.start
|
22
|
+
launcher.join
|
@@ -0,0 +1,24 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
2
|
+
require 'bane'
|
3
|
+
|
4
|
+
include Bane
|
5
|
+
|
6
|
+
# This example creates several behavior listening on distinct ports.
|
7
|
+
# Note the FixedResponse port specifies to listen to all hosts (0.0.0.0), all
|
8
|
+
# other servers listen to localhost only by default (127.0.0.1).
|
9
|
+
|
10
|
+
close_immediately = Behaviors::CloseImmediately.new
|
11
|
+
never_respond = Behaviors::NeverRespond.new
|
12
|
+
fixed_response = Behaviors::FixedResponse.new(:message => "OK")
|
13
|
+
|
14
|
+
launcher = Launcher.new([BehaviorServer.new(3000, close_immediately),
|
15
|
+
BehaviorServer.new(8000, never_respond),
|
16
|
+
BehaviorServer.new(8080, fixed_response, BehaviorServer::ALL_INTERFACES)])
|
17
|
+
launcher.start
|
18
|
+
# To run until interrupt, use the following line:
|
19
|
+
#launcher.join
|
20
|
+
|
21
|
+
# For examples, we'll let these sleep for a few seconds and then shut down'
|
22
|
+
sleep 10
|
23
|
+
launcher.stop
|
24
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
2
|
+
require 'bane'
|
3
|
+
|
4
|
+
include Bane
|
5
|
+
|
6
|
+
# This example creates a single behavior listening on port 3000.
|
7
|
+
# Note that the behavior, CloseAfterPause, specifies a default duration to pause - 60 seconds.
|
8
|
+
|
9
|
+
behavior = BehaviorServer.new(3000, Behaviors::CloseAfterPause.new(:duration => 60))
|
10
|
+
launcher = Launcher.new([behavior])
|
11
|
+
launcher.start
|
12
|
+
# To run until interrupt, use the following line:
|
13
|
+
#launcher.join
|
14
|
+
|
15
|
+
# For examples, we'll let these sleep for a few seconds and then shut down'
|
16
|
+
sleep 10
|
17
|
+
launcher.stop
|
18
|
+
|
data/lib/bane.rb
CHANGED
@@ -2,7 +2,7 @@ require 'bane/compatibility'
|
|
2
2
|
require 'bane/service_registry'
|
3
3
|
require 'bane/behaviors'
|
4
4
|
require 'bane/launcher'
|
5
|
-
require 'bane/
|
6
|
-
require 'bane/
|
5
|
+
require 'bane/behavior_server'
|
6
|
+
require 'bane/command_line_configuration'
|
7
7
|
require 'bane/configuration_parser'
|
8
8
|
require 'bane/naive_http_response'
|
@@ -1,19 +1,24 @@
|
|
1
1
|
require 'gserver'
|
2
2
|
|
3
3
|
module Bane
|
4
|
-
class
|
5
|
-
|
6
|
-
|
4
|
+
class BehaviorServer < GServer
|
5
|
+
|
6
|
+
ALL_INTERFACES = "0.0.0.0"
|
7
|
+
|
8
|
+
def initialize(port, behavior, host = BehaviorServer::DEFAULT_HOST)
|
9
|
+
super(port, host)
|
7
10
|
@behavior = behavior
|
8
|
-
@options = options
|
9
11
|
self.audit = true
|
10
|
-
self.stdlog = logger
|
11
12
|
end
|
12
13
|
|
13
14
|
def serve(io)
|
14
|
-
@behavior.serve(io
|
15
|
+
@behavior.serve(io)
|
15
16
|
end
|
16
17
|
|
18
|
+
def to_s
|
19
|
+
"<Bane::BehaviorServer: port=#{@port}, behavior=#{@behavior.class}>"
|
20
|
+
end
|
21
|
+
|
17
22
|
protected
|
18
23
|
|
19
24
|
alias_method :original_log, :log
|
data/lib/bane/behaviors.rb
CHANGED
@@ -16,16 +16,16 @@ module Bane
|
|
16
16
|
# a "while(io.gets)" loop, which reads a line from the input and
|
17
17
|
# then performs the given behavior.
|
18
18
|
module ForEachLine
|
19
|
-
def serve(io
|
19
|
+
def serve(io)
|
20
20
|
while (io.gets)
|
21
|
-
super(io
|
21
|
+
super(io)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
26
|
# Closes the connection immediately after a connection is made.
|
27
27
|
class CloseImmediately < BasicBehavior
|
28
|
-
def serve(io
|
28
|
+
def serve(io)
|
29
29
|
# do nothing
|
30
30
|
end
|
31
31
|
end
|
@@ -35,10 +35,12 @@ module Bane
|
|
35
35
|
# Options:
|
36
36
|
# - duration: The number of seconds to wait before disconnect. Default: 30
|
37
37
|
class CloseAfterPause < BasicBehavior
|
38
|
-
def
|
39
|
-
options = {:duration => 30}.merge(options)
|
38
|
+
def initialize(options = {})
|
39
|
+
@options = {:duration => 30}.merge(options)
|
40
|
+
end
|
40
41
|
|
41
|
-
|
42
|
+
def serve(io)
|
43
|
+
sleep(@options[:duration])
|
42
44
|
end
|
43
45
|
end
|
44
46
|
|
@@ -47,10 +49,12 @@ module Bane
|
|
47
49
|
# Options:
|
48
50
|
# - message: The response message to send. Default: "Hello, world!"
|
49
51
|
class FixedResponse < BasicBehavior
|
50
|
-
def
|
51
|
-
options = {:message => "Hello, world!"}.merge(options)
|
52
|
+
def initialize(options = {})
|
53
|
+
@options = {:message => "Hello, world!"}.merge(options)
|
54
|
+
end
|
52
55
|
|
53
|
-
|
56
|
+
def serve(io)
|
57
|
+
io.write @options[:message]
|
54
58
|
end
|
55
59
|
end
|
56
60
|
|
@@ -60,7 +64,7 @@ module Bane
|
|
60
64
|
|
61
65
|
# Sends a newline character as the only response
|
62
66
|
class NewlineResponse < BasicBehavior
|
63
|
-
def serve(io
|
67
|
+
def serve(io)
|
64
68
|
io.write "\n"
|
65
69
|
end
|
66
70
|
end
|
@@ -71,7 +75,7 @@ module Bane
|
|
71
75
|
|
72
76
|
# Sends a random response.
|
73
77
|
class RandomResponse < BasicBehavior
|
74
|
-
def serve(io
|
78
|
+
def serve(io)
|
75
79
|
io.write random_string
|
76
80
|
end
|
77
81
|
|
@@ -92,10 +96,13 @@ module Bane
|
|
92
96
|
# - message: The response to send. Default: "Hello, world!"
|
93
97
|
# - pause_duration: The number of seconds to pause between each character. Default: 10 seconds
|
94
98
|
class SlowResponse < BasicBehavior
|
95
|
-
def
|
96
|
-
options = {:message => "Hello, world!", :pause_duration => 10}.merge(options)
|
97
|
-
|
98
|
-
|
99
|
+
def initialize(options = {})
|
100
|
+
@options = {:message => "Hello, world!", :pause_duration => 10}.merge(options)
|
101
|
+
end
|
102
|
+
|
103
|
+
def serve(io)
|
104
|
+
message = @options[:message]
|
105
|
+
pause_duration = @options[:pause_duration]
|
99
106
|
|
100
107
|
message.each_char do |char|
|
101
108
|
io.write char
|
@@ -111,7 +118,7 @@ module Bane
|
|
111
118
|
# Accepts a connection and never sends a byte of data. The connection is
|
112
119
|
# left open indefinitely.
|
113
120
|
class NeverRespond < BasicBehavior
|
114
|
-
def serve(io
|
121
|
+
def serve(io)
|
115
122
|
loop { sleep 1 }
|
116
123
|
end
|
117
124
|
end
|
@@ -121,9 +128,11 @@ module Bane
|
|
121
128
|
# Options
|
122
129
|
# - length: The size in bytes of the response to send. Default: 1,000,000 bytes
|
123
130
|
class DelugeResponse < BasicBehavior
|
124
|
-
def
|
125
|
-
options = {:length => 1_000_000}.merge(options)
|
126
|
-
|
131
|
+
def initialize(options = {})
|
132
|
+
@options = {:length => 1_000_000}.merge(options)
|
133
|
+
end
|
134
|
+
def serve(io)
|
135
|
+
length = @options[:length]
|
127
136
|
|
128
137
|
length.times { io.write('x') }
|
129
138
|
end
|
@@ -150,7 +159,7 @@ module Bane
|
|
150
159
|
</html>
|
151
160
|
EOF
|
152
161
|
|
153
|
-
def serve(io
|
162
|
+
def serve(io)
|
154
163
|
io.gets # Read the request before responding
|
155
164
|
response = NaiveHttpResponse.new(401, "Unauthorized", "text/html", UNAUTHORIZED_RESPONSE_BODY)
|
156
165
|
io.write(response.to_s)
|