bane 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ desc "Run all tests"
4
+ Rake::TestTask.new do |test|
5
+ test.libs << 'test'
6
+ test.test_files = FileList['test/**/*_test.rb']
7
+ test.verbose = true
8
+ end
data/TODO ADDED
@@ -0,0 +1,13 @@
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.
8
+ - Log every request to the server/behavior, in addition to open, close. For this to work, it would have to be the
9
+ behavior's responsibility, since GServer#serve gets called only once for the lifetime of the connection.
10
+
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.
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
4
+ require 'bane'
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
16
+ end
17
+
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'bane'
3
+
4
+ include Bane
5
+
6
+ launcher = Launcher.new(Configuration(3000, Behaviors::CloseImmediately, Behaviors::CloseAfterPause))
7
+ launcher.start
8
+ launcher.join
9
+ # runs until interrupt...
@@ -0,0 +1,7 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'bane'
3
+
4
+ launcher = Bane::Launcher.new(Configuration(3000, "CloseImmediately"))
5
+ launcher.start
6
+ launcher.join
7
+ # runs until interrupt...
@@ -0,0 +1,16 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'bane'
3
+
4
+ include Bane
5
+ include Behaviors
6
+
7
+ launcher = Launcher.new(Configuration(
8
+ 10256 => {:behavior => CloseAfterPause, :duration => 3},
9
+ 10593 => {:behavior => FixedResponse, :message => "Hey!"},
10
+ 10689 => {:behavior => SlowResponse, :message => "Custom message", :pause_duration => 15},
11
+ 11239 => CloseAfterPause # Use the defaults for this behavior, don't need a Hash
12
+ )
13
+ )
14
+ launcher.start
15
+ launcher.join
16
+ # runs until interrupt...
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'bane'
3
+
4
+ include Bane
5
+ include Behaviors
6
+
7
+ launcher = Launcher.new(Configuration(
8
+ 10256 => CloseAfterPause,
9
+ 10689 => CloseAfterPause, # severs may be repeated
10
+ 11999 => CloseImmediately
11
+ )
12
+ )
13
+ launcher.start
14
+ launcher.join
15
+ # runs until interrupt...
@@ -0,0 +1,8 @@
1
+ require 'bane/compatibility'
2
+ require 'bane/service_registry'
3
+ require 'bane/behaviors'
4
+ require 'bane/launcher'
5
+ require 'bane/delegating_gserver'
6
+ require 'bane/configuration'
7
+ require 'bane/configuration_parser'
8
+ require 'bane/naive_http_response'
@@ -0,0 +1,151 @@
1
+ module Bane
2
+
3
+ module Behaviors
4
+
5
+ class BasicBehavior
6
+ def self.inherited(clazz)
7
+ ServiceRegistry.register(clazz)
8
+ end
9
+
10
+ def self.simple_name
11
+ self.name.split("::").last
12
+ end
13
+ end
14
+
15
+ # This module can be used to wrap another behavior with
16
+ # a "while(io.gets)" loop, which reads a line from the input and
17
+ # then performs the given behavior.
18
+ module ForEachLine
19
+ def serve(io, options)
20
+ while (io.gets)
21
+ super(io, options)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Closes the connection immediately after a connection is made.
27
+ class CloseImmediately < BasicBehavior
28
+ def serve(io, options)
29
+ # do nothing
30
+ end
31
+ end
32
+
33
+ # Accepts a connection, pauses a fixed duration, then closes the connection.
34
+ #
35
+ # Options:
36
+ # - duration: The number of seconds to wait before disconnect. Default: 30
37
+ class CloseAfterPause < BasicBehavior
38
+ def serve(io, options)
39
+ options = {:duration => 30}.merge(options)
40
+
41
+ sleep(options[:duration])
42
+ end
43
+ end
44
+
45
+ # Sends a static response.
46
+ #
47
+ # Options:
48
+ # - message: The response message to send. Default: "Hello, world!"
49
+ class FixedResponse < BasicBehavior
50
+ def serve(io, options)
51
+ options = {:message => "Hello, world!"}.merge(options)
52
+
53
+ io.write options[:message]
54
+ end
55
+ end
56
+
57
+ class FixedResponseForEachLine < FixedResponse
58
+ include ForEachLine
59
+ end
60
+
61
+ # Sends a random response.
62
+ class RandomResponse < BasicBehavior
63
+ def serve(io, options)
64
+ io.write random_string
65
+ end
66
+
67
+ private
68
+ def random_string
69
+ (1..rand(26)+1).map { |i| ('a'..'z').to_a[rand(26)] }.join
70
+ end
71
+
72
+ end
73
+
74
+ class RandomResponseForEachLine < RandomResponse
75
+ include ForEachLine
76
+ end
77
+
78
+ # Sends a fixed response chacter-by-character, pausing in between each character.
79
+ #
80
+ # Options:
81
+ # - message: The response to send. Default: "Hello, world!"
82
+ # - pause_duration: The number of seconds to pause between each character. Default: 10 seconds
83
+ class SlowResponse < BasicBehavior
84
+ def serve(io, options)
85
+ options = {:message => "Hello, world!", :pause_duration => 10}.merge(options)
86
+ message = options[:message]
87
+ pause_duration = options[:pause_duration]
88
+
89
+ message.each_char do |char|
90
+ io.write char
91
+ sleep pause_duration
92
+ end
93
+ end
94
+ end
95
+
96
+ class SlowResponseForEachLine < SlowResponse
97
+ include ForEachLine
98
+ end
99
+
100
+ # Accepts a connection and never sends a byte of data. The connection is
101
+ # left open indefinitely.
102
+ class NeverRespond < BasicBehavior
103
+ def serve(io, options)
104
+ loop { sleep 1 }
105
+ end
106
+ end
107
+
108
+ # Sends a large response. Response consists of a repeated 'x' character.
109
+ #
110
+ # Options
111
+ # - length: The size in bytes of the response to send. Default: 1,000,000 bytes
112
+ class DelugeResponse < BasicBehavior
113
+ def serve(io, options)
114
+ options = {:length => 1_000_000}.merge(options)
115
+ length = options[:length]
116
+
117
+ length.times { io.write('x') }
118
+ end
119
+ end
120
+
121
+ class DelugeResponseForEachLine < DelugeResponse
122
+ include ForEachLine
123
+ end
124
+
125
+ # Sends an HTTP 401 response (Unauthorized) for every request. This
126
+ # attempts to mimic an HTTP server by reading a line (the request)
127
+ # and then sending the response. This behavior responds to all
128
+ # incoming request URLs on the running port.
129
+ class HttpRefuseAllCredentials < BasicBehavior
130
+ UNAUTHORIZED_RESPONSE_BODY = <<EOF
131
+ <!DOCTYPE html>
132
+ <html>
133
+ <head>
134
+ <title>Bane Server</title>
135
+ </head>
136
+ <body>
137
+ <h1>Unauthorized</h1>
138
+ </body>
139
+ </html>
140
+ EOF
141
+
142
+ def serve(io, options)
143
+ io.gets # Read the request before responding
144
+ response = NaiveHttpResponse.new(401, "Unauthorized", "text/html", UNAUTHORIZED_RESPONSE_BODY)
145
+ io.write(response.to_s)
146
+ end
147
+ end
148
+
149
+ end
150
+ end
151
+
@@ -0,0 +1,20 @@
1
+ class String
2
+ unless "".respond_to?(:each_char)
3
+ # copied from jcode in Ruby 1.8.6
4
+ def each_char
5
+ if block_given?
6
+ scan(/./m) do |x|
7
+ yield x
8
+ end
9
+ else
10
+ scan(/./m)
11
+ end
12
+ end
13
+ end
14
+
15
+ unless "".respond_to?(:lines)
16
+ require "enumerator"
17
+
18
+ alias_method :lines, :to_a
19
+ end
20
+ end
@@ -0,0 +1,50 @@
1
+ module Bane
2
+
3
+ class Configuration
4
+
5
+ def initialize(configurations)
6
+ @configurations = configurations
7
+ end
8
+
9
+ def start(logger)
10
+ @configurations.map do |config|
11
+ config.start(logger)
12
+ end
13
+ end
14
+
15
+ class ConfigurationRecord
16
+
17
+ def initialize(port, behavior, options = {})
18
+ @port = port
19
+ @behavior = behavior
20
+ @options = options
21
+ end
22
+
23
+ def start(logger)
24
+ new_server = DelegatingGServer.new(@port, @behavior.new, @options, logger)
25
+ new_server.start
26
+ new_server
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ class ConfigurationError < RuntimeError; end
33
+
34
+ class UnknownBehaviorError < RuntimeError
35
+ def initialize(name)
36
+ super "Unknown behavior: #{name}"
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ # Helper method to easily create configuration.
43
+ #
44
+ # This should likely take the constructor block from the Configuration class
45
+ # and then we can simplify this class so it's not so big.
46
+ module Kernel
47
+ def Configuration(*args)
48
+ Bane::Configuration.new(Bane::ConfigurationParser.new(*args).configurations)
49
+ end
50
+ end
@@ -0,0 +1,74 @@
1
+ module Bane
2
+
3
+ class ConfigurationParser
4
+
5
+ attr_reader :configurations
6
+
7
+
8
+ def initialize(*args)
9
+ @configurations = []
10
+
11
+ @configurations = case args[0]
12
+ when String, Integer
13
+ map_integer_and_behavior_arguments(*args)
14
+ when Hash
15
+ map_hash_arguments(args[0])
16
+ else
17
+ raise ConfigurationError, "Unknown configuration arguments #{args.inspect}"
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def map_integer_and_behavior_arguments(*args)
24
+ port = Integer(args.shift)
25
+
26
+ behavior_classes = args
27
+ behavior_classes = ServiceRegistry.all_servers if behavior_classes.empty?
28
+
29
+ setup_linear_ports(port, behavior_classes)
30
+ end
31
+
32
+ def setup_linear_ports(port, behavior_classes)
33
+ behavior_classes.each_with_index do |behavior, index|
34
+ @configurations << Bane::Configuration::ConfigurationRecord.new(port + index, find(behavior))
35
+ end
36
+ @configurations
37
+ end
38
+
39
+ def find(behavior)
40
+ case behavior
41
+ when String
42
+ raise UnknownBehaviorError.new(behavior) unless Behaviors.const_defined?(behavior.to_sym)
43
+ Behaviors.const_get(behavior)
44
+ when Module
45
+ behavior
46
+ else
47
+ raise UnknownBehaviorError.new(behavior)
48
+ end
49
+ end
50
+
51
+ def map_hash_arguments(options)
52
+ options.each_pair do |key, value|
53
+ @configurations << extract_configuration_from(key, value)
54
+ end
55
+ @configurations
56
+ end
57
+
58
+ def extract_configuration_from(port, value)
59
+ case value
60
+ when Module
61
+ behavior = value
62
+ Bane::Configuration::ConfigurationRecord.new(port, behavior)
63
+ when Hash
64
+ behavior = value.delete(:behavior)
65
+ options = value
66
+ Bane::Configuration::ConfigurationRecord.new(port, behavior, options)
67
+ else
68
+ raise ConfigurationError, "Unknown configuration option: #{value.inspect}"
69
+ end
70
+ end
71
+
72
+ end
73
+
74
+ end
@@ -0,0 +1,42 @@
1
+ require 'gserver'
2
+
3
+ module Bane
4
+ class DelegatingGServer < GServer
5
+ def initialize(port, behavior, options = {}, logger = $stderr)
6
+ super(port)
7
+ @behavior = behavior
8
+ @options = options
9
+ self.audit = true
10
+ self.stdlog = logger
11
+ end
12
+
13
+ def serve(io)
14
+ @behavior.serve(io, @options)
15
+ end
16
+
17
+ protected
18
+
19
+ alias_method :original_log, :log
20
+
21
+ def log(message)
22
+ original_log("#{@behavior.class.simple_name} #{@host}:#{@port} #{message}")
23
+ end
24
+
25
+ def connecting(client)
26
+ addr = client.peeraddr
27
+ log("client:#{addr[1]} #{addr[2]}<#{addr[3]}> connect")
28
+ end
29
+
30
+ def disconnecting(client_port)
31
+ log("client:#{client_port} disconnect")
32
+ end
33
+
34
+ def starting()
35
+ log("start")
36
+ end
37
+
38
+ def stopping()
39
+ log("stop")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,25 @@
1
+ module Bane
2
+
3
+ class Launcher
4
+
5
+ def initialize(configurations, logger = $stderr)
6
+ @configuration = configurations
7
+ @logger = logger
8
+ @running_servers = []
9
+ end
10
+
11
+ def start
12
+ @running_servers = @configuration.start(@logger)
13
+ end
14
+
15
+ def join
16
+ @running_servers.each { |server| server.join }
17
+ end
18
+
19
+ def stop
20
+ @running_servers.each { |server| server.stop }
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,46 @@
1
+ class NaiveHttpResponse
2
+
3
+ CRLF = "\r\n"
4
+
5
+ class HttpHeader
6
+ def initialize(headers)
7
+ @headers = headers
8
+ end
9
+
10
+ def to_s
11
+ @headers.map { |k, v| "#{k}: #{v}" }.join(CRLF)
12
+ end
13
+
14
+ end
15
+
16
+ def initialize(response_code, response_description, content_type, body)
17
+ @code = response_code
18
+ @description = response_description
19
+ @content_type = content_type
20
+ @body = body
21
+ end
22
+
23
+ def to_s
24
+ str = []
25
+ str << "HTTP/1.1 #{@code} #{@description}"
26
+ str << http_header.to_s
27
+ str << ""
28
+ str << @body
29
+ str.join(CRLF)
30
+ end
31
+
32
+ private
33
+
34
+ def http_header()
35
+ HttpHeader.new(
36
+ {"Server" => "Bane HTTP Server",
37
+ "Connection" => "close",
38
+ "Date" => http_date(Time.now),
39
+ "Content-Type" => @content_type,
40
+ "Content-Length" => @body.length})
41
+ end
42
+
43
+ def http_date(time)
44
+ time.gmtime.strftime("%a, %d %b %Y %H:%M:%S GMT")
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ module Bane
2
+
3
+ module ServiceRegistry
4
+ def self.all_servers
5
+ @servers ||= []
6
+ end
7
+
8
+ def self.register(server)
9
+ all_servers << server unless all_servers.include?(server)
10
+ end
11
+
12
+ def self.unregister(server)
13
+ all_servers.delete server
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,137 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require 'timeout'
3
+ require 'mocha'
4
+
5
+ class BehaviorsTest < Test::Unit::TestCase
6
+
7
+ include Bane::Behaviors
8
+
9
+ class FakeConnection < StringIO
10
+ def will_send(query)
11
+ @query = query
12
+ end
13
+
14
+ def gets
15
+ @query
16
+ @read_query = true
17
+ end
18
+
19
+ def read_query?
20
+ @read_query
21
+ end
22
+ end
23
+
24
+ def setup
25
+ @fake_connection = FakeConnection.new
26
+ end
27
+
28
+ def test_fixed_response_sends_the_specified_message
29
+ query_server(create(FixedResponse), :message => "Test Message")
30
+
31
+ assert_equal "Test Message", response
32
+ end
33
+
34
+ def test_deluge_response_sends_one_million_bytes_by_default
35
+ query_server(create DelugeResponse)
36
+
37
+ assert_response_length 1_000_000
38
+ end
39
+
40
+ def test_deluge_response_accepts_length_parameter
41
+ query_server(create(DelugeResponse), :length => 1)
42
+
43
+ assert_response_length 1
44
+ end
45
+
46
+ def test_close_immediately_sends_no_response
47
+ query_server(create CloseImmediately)
48
+
49
+ assert_empty_response()
50
+ end
51
+
52
+ def test_never_respond_never_sends_a_response
53
+ server = create NeverRespond
54
+
55
+ assert_raise Timeout::Error do
56
+ Timeout::timeout(3) { query_server(server) }
57
+ end
58
+ assert_empty_response
59
+ end
60
+
61
+ def test_close_after_pause_sleeps_30_seconds_by_default_and_sends_nothing
62
+ server = create CloseAfterPause
63
+ server.expects(:sleep).with(30)
64
+
65
+ query_server(server)
66
+
67
+ assert_empty_response
68
+ end
69
+
70
+ def test_close_after_pause_accepts_duration_parameter
71
+ server = create CloseAfterPause
72
+
73
+ within(2) { query_server(server, :duration => 1) }
74
+ end
75
+
76
+ def test_slow_response_sends_a_message_slowly
77
+ server = create SlowResponse
78
+ message = "Hi!"
79
+ delay = 0.5
80
+ max_delay = (message.length + 1) * delay
81
+
82
+ within(max_delay) { query_server(server, :pause_duration => delay, :message => message)}
83
+
84
+ assert_equal message, response
85
+ end
86
+
87
+ def test_random_response_sends_a_nonempty_response
88
+ query_server(create RandomResponse)
89
+
90
+ assert (!response.empty?), "Should have served a nonempty response"
91
+ end
92
+
93
+ def test_refuse_all_http_credentials_sends_401_response_code
94
+ @fake_connection.will_send("GET /some/irrelevant/path HTTP/1.1")
95
+
96
+ server = create(HttpRefuseAllCredentials)
97
+ query_server(server)
98
+
99
+ assert @fake_connection.read_query?, "Should have read the HTTP query before sending response"
100
+ assert_match /HTTP\/1.1 401 Unauthorized/, response, 'Should have responded with the 401 response code'
101
+ end
102
+
103
+ def test_simple_name_strips_away_the_namespace
104
+ assert_equal "SlowResponse", Bane::Behaviors::SlowResponse.simple_name
105
+ end
106
+
107
+ private
108
+
109
+ def create(server_class)
110
+ server_class.new()
111
+ end
112
+
113
+ def query_server(server, options = {})
114
+ server.serve(@fake_connection, options)
115
+ end
116
+
117
+ def response
118
+ @fake_connection.string
119
+ end
120
+
121
+ def assert_empty_response
122
+ assert_equal 0, response.length, "Should have sent nothing"
123
+ end
124
+
125
+ def assert_response_length(expected_length)
126
+ assert_equal expected_length, response.length, "Response was the wrong length"
127
+ end
128
+
129
+ def within(duration)
130
+ begin
131
+ Timeout::timeout(duration) { yield }
132
+ rescue Timeout::Error
133
+ flunk "Test took too long - should have completed within #{duration} seconds."
134
+ end
135
+ end
136
+
137
+ end
@@ -0,0 +1,124 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require 'mocha'
3
+
4
+ class ConfigurationParserTest < Test::Unit::TestCase
5
+
6
+ include Bane
7
+
8
+ IRRELEVANT_BEHAVIOR = Module.new
9
+
10
+ def test_should_map_single_port_and_server_name
11
+ parser = ConfigurationParser.new(3000, "CloseAfterPause")
12
+ assert_matches_configuration([
13
+ {:port => 3000, :behavior => Behaviors::CloseAfterPause}
14
+ ], parser)
15
+ end
16
+
17
+ def test_should_map_multiple_servers_given_one_starting_port
18
+ parser = ConfigurationParser.new(3000, "CloseImmediately", "CloseAfterPause")
19
+ assert_matches_configuration([
20
+ {:port => 3000, :behavior => Behaviors::CloseImmediately},
21
+ {:port => 3001, :behavior => Behaviors::CloseAfterPause}
22
+ ], parser)
23
+ end
24
+
25
+ def test_should_map_string_port
26
+ parser = Bane::ConfigurationParser.new("3000", IRRELEVANT_BEHAVIOR)
27
+ actual = parser.configurations[0]
28
+ assert_equal 3000, actual.instance_variable_get(:@port), "Should have mapped port given a String"
29
+ end
30
+
31
+ def test_should_raise_if_unknown_server_name
32
+ assert_raises Bane::UnknownBehaviorError do
33
+ Bane::ConfigurationParser.new(IRRELEVANT_PORT, "ABehaviorThatDoesNotExist")
34
+ end
35
+ end
36
+
37
+ def test_should_map_server_when_given_class
38
+ parser = ConfigurationParser.new(IRRELEVANT_PORT, Behaviors::CloseAfterPause)
39
+ actual = parser.configurations[0]
40
+ assert_equal Behaviors::CloseAfterPause, actual.instance_variable_get(:@behavior), "Wrong behavior"
41
+ end
42
+
43
+ def test_should_ask_service_registry_for_all_behaviors_if_none_specified
44
+ fake_behavior = unique_behavior
45
+ another_fake_behavior = unique_behavior
46
+
47
+ ServiceRegistry.expects(:all_servers).returns([fake_behavior, another_fake_behavior])
48
+ parser = ConfigurationParser.new(4000)
49
+ assert_matches_configuration([
50
+ {:port => 4000, :behavior => fake_behavior},
51
+ {:port => 4001, :behavior => another_fake_behavior}
52
+ ], parser)
53
+ end
54
+
55
+ def test_should_raise_exception_if_no_arguments
56
+ assert_raises ConfigurationError do
57
+ ConfigurationParser.new
58
+ end
59
+ end
60
+
61
+ def test_should_raise_exception_if_nil_port_with_behaviors
62
+ assert_raises ConfigurationError do
63
+ ConfigurationParser.new(nil, IRRELEVANT_BEHAVIOR)
64
+ end
65
+ end
66
+
67
+ def test_should_map_single_hash_entry_as_port_and_behavior
68
+ parser = ConfigurationParser.new(
69
+ 10256 => Behaviors::CloseAfterPause
70
+ )
71
+
72
+ assert_matches_configuration([
73
+ {:port => 10256, :behavior => Behaviors::CloseAfterPause}
74
+ ], parser)
75
+ end
76
+
77
+ def test_should_map_multiple_hash_entries_as_port_and_behavior
78
+ parser = ConfigurationParser.new(
79
+ 10256 => Behaviors::CloseAfterPause,
80
+ 6450 => Behaviors::CloseImmediately
81
+ )
82
+
83
+ assert_matches_configuration([
84
+ {:port => 10256, :behavior => Behaviors::CloseAfterPause},
85
+ {:port => 6450, :behavior => Behaviors::CloseImmediately}
86
+ ], parser)
87
+ end
88
+
89
+ def test_should_map_hash_with_options
90
+ parser = ConfigurationParser.new(
91
+ 10256 => {:behavior => Behaviors::CloseAfterPause, :duration => 3},
92
+ 11239 => Behaviors::CloseImmediately
93
+ )
94
+
95
+ assert_matches_configuration([
96
+ {:port => 10256, :behavior => Behaviors::CloseAfterPause},
97
+ {:port => 11239, :behavior => Behaviors::CloseImmediately}
98
+ ], parser)
99
+ end
100
+
101
+ private
102
+
103
+ def assert_matches_configuration(expected_config, actual_config)
104
+ actual_elements = actual_config.configurations
105
+ assert_equal expected_config.size, actual_elements.size, "Did not create correct number of configurations. Actual: #{actual_elements}, expected #{expected_config}"
106
+
107
+ expected_config.each do |expected|
108
+ # We make no guarantee on the order of the configurations
109
+ assert_includes_configuration(actual_elements, expected)
110
+ end
111
+ end
112
+
113
+ def assert_includes_configuration(actual_elements, expected)
114
+ expected_port = expected[:port]
115
+ matching_config = actual_elements.detect { |actual| actual.instance_variable_get(:@port) == expected_port }
116
+ assert_not_nil matching_config, "Should have found a configuration with port #{expected_port}"
117
+ assert_equal expected[:behavior], matching_config.instance_variable_get(:@behavior), "Wrong behavior for port #{expected_port}"
118
+ end
119
+
120
+ def unique_behavior
121
+ Module.new
122
+ end
123
+
124
+ end
@@ -0,0 +1,52 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require 'mocha'
3
+
4
+ module Bane
5
+ module Behaviors
6
+ class FakeTestServer < BasicBehavior
7
+ end
8
+ end
9
+ end
10
+
11
+ class ConfigurationTest < Test::Unit::TestCase
12
+
13
+ include Bane
14
+
15
+ def test_starts_server_on_specified_port
16
+ target_port = 4000
17
+ DelegatingGServer.expects(:new).with(equals(target_port), anything(), anything(), anything()).returns(fake_server)
18
+
19
+ start_configuration(target_port, Behaviors::FakeTestServer)
20
+ end
21
+
22
+ def test_starts_server_with_specified_behavior
23
+ behavior_instance = stub('constructed behavior instance')
24
+ Behaviors::FakeTestServer.expects(:new).returns(behavior_instance)
25
+ DelegatingGServer.expects(:new).with(anything(), equals(behavior_instance), anything(), anything()).returns(fake_server)
26
+
27
+ start_configuration(IRRELEVANT_PORT, Behaviors::FakeTestServer)
28
+ end
29
+
30
+ def test_constructs_server_with_specified_options
31
+ options = { :an_option => :a_value, :another_option => :another_value }
32
+ DelegatingGServer.expects(:new).with(anything(), anything, has_entries(options), anything()).returns(fake_server)
33
+
34
+ start_configuration(IRRELEVANT_PORT, Behaviors::FakeTestServer, options)
35
+ end
36
+
37
+ private
38
+
39
+ def start_configuration(target_port, behavior, options = {})
40
+ configuration = configuration_with(target_port, behavior, options)
41
+ configuration.start(StringIO.new)
42
+ end
43
+
44
+ def configuration_with(port, behavior, options)
45
+ Configuration.new([Configuration::ConfigurationRecord.new(port, behavior, options)])
46
+ end
47
+
48
+ def fake_server
49
+ stub_everything('fake_server')
50
+ end
51
+
52
+ end
@@ -0,0 +1,56 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require 'mocha'
3
+
4
+ class DelegatingGserverTest < Test::Unit::TestCase
5
+ include Bane
6
+
7
+ IRRELEVANT_IO_STREAM = nil
8
+ IRRELEVANT_OPTIONS = {}
9
+
10
+ def test_serve_passes_a_hash_of_options_even_if_not_initialized_with_options
11
+ behavior = mock()
12
+ server = DelegatingGServer.new(IRRELEVANT_PORT, behavior)
13
+
14
+ behavior.expects(:serve).with(anything(), is_a(Hash))
15
+
16
+ server.serve(IRRELEVANT_IO_STREAM)
17
+ end
18
+
19
+ def test_serve_passes_constructor_options_to_behaviors_serve_method
20
+ behavior = mock()
21
+
22
+ initialized_options = {:expected => :options}
23
+ server = DelegatingGServer.new(IRRELEVANT_PORT, behavior, initialized_options)
24
+
25
+ behavior.expects(:serve).with(anything(), equals(initialized_options))
26
+
27
+ server.serve(IRRELEVANT_IO_STREAM)
28
+ end
29
+
30
+ def test_connection_log_messages_use_short_behavior_name_to_shorten_log_messages
31
+ [:connecting, :disconnecting].each do |method|
32
+ assert_log_message_uses_short_behavior_name_for(method) do |server|
33
+ server.send(method, stub_everything(:peeraddr => [127, 0, 0, 1]))
34
+ end
35
+ end
36
+ end
37
+
38
+ def test_start_stop_log_messages_use_short_behavior_name_to_shorten_log_messages
39
+ [:starting, :stopping].each do |method|
40
+ assert_log_message_uses_short_behavior_name_for(method) do |server|
41
+ server.send(method)
42
+ end
43
+ end
44
+ end
45
+
46
+ def assert_log_message_uses_short_behavior_name_for(method)
47
+ logger = StringIO.new
48
+ server = DelegatingGServer.new(IRRELEVANT_PORT, Bane::Behaviors::CloseImmediately.new, IRRELEVANT_OPTIONS, logger)
49
+
50
+ yield server
51
+
52
+ assert_match /CloseImmediately/, logger.string, "Log for #{method} should contain class short name"
53
+ assert_no_match /Behaviors::CloseImmediately/, logger.string, "Log for #{method} should not contain expanded module name"
54
+ end
55
+
56
+ end
@@ -0,0 +1,67 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require 'net/telnet'
3
+ require 'open-uri'
4
+
5
+ class BaneIntegrationTest < Test::Unit::TestCase
6
+
7
+ TEST_PORT = 4000
8
+
9
+ def test_uses_specified_port_and_server
10
+ run_server_with(TEST_PORT, "FixedResponse") do
11
+ telnet_to TEST_PORT do |response|
12
+ assert !response.empty?, "Should have had a non-empty response"
13
+ end
14
+ end
15
+ end
16
+
17
+ def test_uses_behavior_options
18
+ expected_message = "Expected test message"
19
+ options = {TEST_PORT => {:behavior => Bane::Behaviors::FixedResponse,
20
+ :message => expected_message}}
21
+
22
+ run_server_with(options) do
23
+ telnet_to TEST_PORT do |response|
24
+ assert_equal expected_message, response, "Wrong response from server"
25
+ end
26
+ end
27
+ end
28
+
29
+ def test_serves_http_requests
30
+ run_server_with(TEST_PORT, "HttpRefuseAllCredentials") do
31
+ begin
32
+ open("http://localhost:#{TEST_PORT}/some/url").read
33
+ flunk "Should have refused access"
34
+ rescue OpenURI::HTTPError => e
35
+ assert_match /401/, e.message
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ private
42
+
43
+ def run_server_with(*options)
44
+ begin
45
+ launcher = Bane::Launcher.new(Configuration(*options), quiet_logger)
46
+ launcher.start
47
+ yield
48
+ ensure
49
+ launcher.stop if launcher
50
+ end
51
+ end
52
+
53
+ def quiet_logger
54
+ StringIO.new
55
+ end
56
+
57
+ def telnet_to(port)
58
+ begin
59
+ telnet = Net::Telnet::new("Host" => "localhost",
60
+ "Port" => port,
61
+ "Timeout" => 5)
62
+ yield telnet.read
63
+ ensure
64
+ telnet.close
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,16 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require 'mocha'
3
+
4
+ class LauncherTest < Test::Unit::TestCase
5
+
6
+ include Bane
7
+
8
+ def test_start_delegates_to_configuration
9
+ configuration = mock()
10
+ logger = stub()
11
+ launcher = Launcher.new(configuration, logger)
12
+
13
+ configuration.expects(:start).with(equals(logger))
14
+ launcher.start
15
+ end
16
+ end
@@ -0,0 +1,69 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class NaiveHttpResponseTest < Test::Unit::TestCase
4
+
5
+ IRRELEVANT_RESPONSE_CODE = "999"
6
+ IRRELEVANT_RESPONSE_DESCRIPTION = "Irrelevant description"
7
+ IRRELEVANT_CONTENT_TYPE = "irrelevant content type"
8
+ IRRELEVANT_BODY = "irrelevant body"
9
+
10
+ def test_should_send_http_format_string
11
+ response = response_for("200", "OK", IRRELEVANT_CONTENT_TYPE, IRRELEVANT_BODY)
12
+ assert_equal "HTTP/1.1 200 OK\r\n", response.lines.first, "First line should be HTTP status"
13
+ end
14
+
15
+ def test_should_include_date
16
+ assert_match /Date: .*/, any_response, 'Should have included a Date header'
17
+ end
18
+
19
+ def test_should_set_content_type
20
+ response = response_for(IRRELEVANT_RESPONSE_CODE,
21
+ IRRELEVANT_RESPONSE_DESCRIPTION,
22
+ "text/xml",
23
+ IRRELEVANT_BODY)
24
+ assert_match /Content-Type: text\/xml/, response, 'Should have included content type'
25
+ end
26
+
27
+ def test_should_set_content_length_as_length_of_body_in_bytes
28
+ message = "Hello, there!"
29
+ response = response_for(IRRELEVANT_RESPONSE_CODE,
30
+ IRRELEVANT_RESPONSE_DESCRIPTION,
31
+ IRRELEVANT_CONTENT_TYPE,
32
+ message)
33
+
34
+ assert_match /Content-Length: #{message.length}/, response, 'Should have included content length'
35
+ end
36
+
37
+ def test_should_include_newline_between_headers_and_body
38
+ message = "This is some body content."
39
+ response = response_for(IRRELEVANT_RESPONSE_CODE,
40
+ IRRELEVANT_RESPONSE_DESCRIPTION,
41
+ IRRELEVANT_CONTENT_TYPE,
42
+ message)
43
+
44
+ response_lines = response.lines.to_a
45
+ index_of_body_start = response_lines.index(message)
46
+ line_before_body = response_lines[index_of_body_start - 1]
47
+ assert line_before_body.strip.empty?, "Should have had blank line before the body"
48
+ end
49
+
50
+ def test_should_include_the_body_at_the_end_of_the_response
51
+ message = "This is some body content."
52
+ response = response_for(IRRELEVANT_RESPONSE_CODE,
53
+ IRRELEVANT_RESPONSE_DESCRIPTION,
54
+ IRRELEVANT_CONTENT_TYPE,
55
+ message)
56
+ assert_match /#{message}$/, response, "Should have ended the response with the body content"
57
+ end
58
+
59
+ private
60
+
61
+ def response_for(response_code, response_description, content_type, body)
62
+ NaiveHttpResponse.new(response_code, response_description, content_type, body).to_s
63
+ end
64
+
65
+ def any_response
66
+ response_for(IRRELEVANT_RESPONSE_CODE, IRRELEVANT_RESPONSE_DESCRIPTION, IRRELEVANT_CONTENT_TYPE, IRRELEVANT_BODY)
67
+ end
68
+
69
+ end
@@ -0,0 +1,20 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ class ServiceRegistryTest < Test::Unit::TestCase
4
+
5
+ include Bane
6
+
7
+ def test_should_add_and_remove_behaviors
8
+ fake = fake_behavior
9
+
10
+ ServiceRegistry.register(fake)
11
+ assert ServiceRegistry.all_servers.include?(fake), "Should have added the new behavior"
12
+
13
+ ServiceRegistry.unregister(fake)
14
+ assert !(ServiceRegistry.all_servers.include?(fake)), "Should have removed the new behavior"
15
+ end
16
+
17
+ def fake_behavior
18
+ Class.new
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'stringio'
4
+
5
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
6
+ require 'bane'
7
+
8
+
9
+ IRRELEVANT_PORT = 4001
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bane
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Wellman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-05-16 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mocha
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.9.8
24
+ version:
25
+ description: " Bane is a test harness used to test your application's interaction with \n other servers. It is based upon the material from Michael Nygard's \"Release\n It!\" book as described in the \"Test Harness\" chapter.\n"
26
+ email: dan@danielwellman.com
27
+ executables:
28
+ - bane
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - TODO
33
+ files:
34
+ - lib/bane/behaviors.rb
35
+ - lib/bane/compatibility.rb
36
+ - lib/bane/configuration.rb
37
+ - lib/bane/configuration_parser.rb
38
+ - lib/bane/delegating_gserver.rb
39
+ - lib/bane/launcher.rb
40
+ - lib/bane/naive_http_response.rb
41
+ - lib/bane/service_registry.rb
42
+ - lib/bane.rb
43
+ - bin/bane
44
+ - test/bane/behaviors_test.rb
45
+ - test/bane/configuration_parser_test.rb
46
+ - test/bane/configuration_test.rb
47
+ - test/bane/delegating_gserver_test.rb
48
+ - test/bane/integration_test.rb
49
+ - test/bane/launcher_test.rb
50
+ - test/bane/naive_http_response_test.rb
51
+ - test/bane/service_registry_test.rb
52
+ - test/test_helper.rb
53
+ - examples/simple_port_and_class_as_constant.rb
54
+ - examples/simple_port_and_class_as_string.rb
55
+ - examples/specify_behavior_options.rb
56
+ - examples/specify_ports.rb
57
+ - Rakefile
58
+ - TODO
59
+ has_rdoc: true
60
+ homepage: http://github.com/danielwellman/bane
61
+ licenses: []
62
+
63
+ post_install_message:
64
+ rdoc_options: []
65
+
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: "0"
73
+ version:
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ version:
80
+ requirements: []
81
+
82
+ rubyforge_project:
83
+ rubygems_version: 1.3.5
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: A test harness for socket connections based upon ideas from Michael Nygard's 'Release It!'
87
+ test_files: []
88
+