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 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
- `sudo gem install bane`
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 initialize Bane with a Hash of port numbers, behavior names, and configuration parameters. For example, you might want to specify a response for the FixedResponse behavior:
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
- require 'bane'
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
- include Bane
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
- launcher = Launcher.new(Configuration(
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
- See `examples/specify_behavior_options.rb` for another example. For a list of options supported by the
46
- basic behaviors, see the source for the behaviors in `Bane::Behaviors` at `lib/bane/behaviors.rb`.
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
- begin
5
- require 'jeweler'
6
- Jeweler::Tasks.new do |gem|
7
- gem.name = "bane"
8
- gem.summary = "A test harness for socket connections based upon ideas from Michael Nygard's 'Release It!'"
9
- gem.description = <<-END
10
- Bane is a test harness used to test your application's interaction with
11
- other servers. It is based upon the material from Michael Nygard's "Release
12
- It!" book as described in the "Test Harness" chapter.
13
- END
14
- gem.authors = ["Daniel Wellman"]
15
- gem.email = "dan@danielwellman.com"
16
- gem.homepage = "http://github.com/danielwellman/bane"
17
- gem.files = FileList[ 'lib/**/*', 'bin/*', 'test/**/*', 'examples/*',
18
- 'Rakefile' ]
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
- desc "Run all tests"
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 'rake/rdoctask'
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 = "bane #{version}"
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
- - Remove some duplication / clarify the ConfigurationParser class as needed
2
- - Dynamically create new behaviors like "SlowResponseForEachLine" by using Module.const_missing
3
- - Use ActiveSupport or create simple extensions for time durations (seconds, minutes, etc.)
4
- - Write the bad TCP/IP behaviors - using something like C/C++.
5
- - Write the remaining bad HTTP behaviors. In addition, we may want to replace the NaiveHttpResponse with something
6
- from the standard Ruby library, so that there's less code in this project, and so we know that we're
7
- following the HTTP protocol.
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
- Ideas:
12
- - Make the Launcher use a Factory to create the server given a dependency, rather than depending directly on the
13
- DelegatingGServer class. This would make it possible to swap the base server implementation more easily.
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
- if ARGV.empty?
7
- puts "Usage: bane port_number <servers>"
8
- puts
9
- puts "All behaviors:"
10
- behavior_names = Bane::ServiceRegistry.all_servers.map(&:simple_name)
11
- behavior_names.sort.each { |behavior| puts " - #{behavior}" }
12
- else
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,9 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'bane'
3
+
4
+ include Bane
5
+
6
+ launcher = Launcher.new(
7
+ [BehaviorServer.new(3000, Behaviors::FixedResponse.new(:message => "Shall we play a game?"))])
8
+ launcher.start
9
+ launcher.join
@@ -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
+
@@ -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/delegating_gserver'
6
- require 'bane/configuration'
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 DelegatingGServer < GServer
5
- def initialize(port, behavior, options = {}, logger = $stderr)
6
- super(port)
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, @options)
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
@@ -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, options)
19
+ def serve(io)
20
20
  while (io.gets)
21
- super(io, options)
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, options)
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 serve(io, options)
39
- options = {:duration => 30}.merge(options)
38
+ def initialize(options = {})
39
+ @options = {:duration => 30}.merge(options)
40
+ end
40
41
 
41
- sleep(options[:duration])
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 serve(io, options)
51
- options = {:message => "Hello, world!"}.merge(options)
52
+ def initialize(options = {})
53
+ @options = {:message => "Hello, world!"}.merge(options)
54
+ end
52
55
 
53
- io.write options[:message]
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, options)
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, options)
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 serve(io, options)
96
- options = {:message => "Hello, world!", :pause_duration => 10}.merge(options)
97
- message = options[:message]
98
- pause_duration = options[:pause_duration]
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, options)
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 serve(io, options)
125
- options = {:length => 1_000_000}.merge(options)
126
- length = options[:length]
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, options)
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)