right_develop 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +4 -0
- data/CHANGELOG.rdoc +3 -0
- data/Gemfile +26 -0
- data/Gemfile.lock +92 -0
- data/LICENSE +20 -0
- data/README.rdoc +50 -0
- data/Rakefile +67 -0
- data/VERSION +1 -0
- data/features/cucumber.feature +27 -0
- data/features/rake_integration.feature +27 -0
- data/features/rspec1.feature +67 -0
- data/features/rspec2.feature +67 -0
- data/features/step_definitions/http_client_steps.rb +27 -0
- data/features/step_definitions/request_balancer_steps.rb +93 -0
- data/features/step_definitions/ruby_steps.rb +206 -0
- data/features/step_definitions/serialization_steps.rb +96 -0
- data/features/step_definitions/server_steps.rb +134 -0
- data/features/support/env.rb +84 -0
- data/features/support/file_utils_bundler_mixin.rb +45 -0
- data/lib/right_develop.rb +32 -0
- data/lib/right_develop/ci.rb +40 -0
- data/lib/right_develop/ci/java_cucumber_formatter.rb +60 -0
- data/lib/right_develop/ci/java_spec_formatter.rb +191 -0
- data/lib/right_develop/ci/rake_task.rb +127 -0
- data/lib/right_develop/net.rb +60 -0
- data/lib/right_develop/parsers.rb +10 -0
- data/lib/right_develop/parsers/sax_parser.rb +139 -0
- data/lib/right_develop/parsers/xml_post_parser.rb +113 -0
- data/right_develop.gemspec +140 -0
- data/right_develop.rconf +8 -0
- data/spec/right_develop/parsers/sax_parser_spec.rb +202 -0
- data/spec/spec_helper.rb +28 -0
- metadata +593 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
When /^a client makes a (buggy )?request to '(.*)'$/ do |buggy, path|
|
2
|
+
t = RightDevelop::Net::HTTPClient::DEFAULT_OPTIONS[:timeout]
|
3
|
+
o = RightDevelop::Net::HTTPClient::DEFAULT_OPTIONS[:open_timeout]
|
4
|
+
When "a client makes a #{buggy}request to '#{path}' with timeout #{t} and open_timeout #{o}"
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
When /^a client makes a (buggy )?request to '(.*)' with timeout (\d+) and open_timeout (\d+)$/ do |buggy, path, timeout, open_timeout|
|
9
|
+
buggy = !(buggy.nil? || buggy.empty?)
|
10
|
+
|
11
|
+
@mock_servers.should_not be_nil
|
12
|
+
@mock_servers.size.should == 1
|
13
|
+
|
14
|
+
timeout = timeout.to_i
|
15
|
+
open_timeout = open_timeout.to_i
|
16
|
+
url = @mock_servers.first.url
|
17
|
+
|
18
|
+
@http_client = RightDevelop::Net::HTTPClient.new(:timeout=>timeout, :open_timeout=>open_timeout)
|
19
|
+
@request_t0 = Time.now
|
20
|
+
begin
|
21
|
+
raise ArgumentError, "Fall down go boom!" if buggy
|
22
|
+
@http_client.get("#{url}#{path}", {:timeout => timeout, :open_timeout => open_timeout})
|
23
|
+
rescue Exception => e
|
24
|
+
@request_error = e
|
25
|
+
end
|
26
|
+
@request_t1 = Time.now
|
27
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# Rails' constantize method to convert passed in policy to the actual class
|
2
|
+
def constantize(camel_cased_word)
|
3
|
+
names = camel_cased_word.split('::')
|
4
|
+
names.shift if names.empty? || names.first.empty?
|
5
|
+
|
6
|
+
constant = Object
|
7
|
+
names.each do |name|
|
8
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
9
|
+
end
|
10
|
+
constant
|
11
|
+
end
|
12
|
+
|
13
|
+
Given /^(\w+) balancing policy$/ do |policy|
|
14
|
+
@health_check = Proc.new do |endpoint|
|
15
|
+
begin
|
16
|
+
RightDevelop::Net::HTTPClient.new.get(endpoint, {:timeout => 1, :open_timeout => 1})
|
17
|
+
true
|
18
|
+
rescue Exception => e
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
@options ||= { :policy => constantize("RightDevelop::Net::LB::" + policy), :health_check => @health_check }
|
24
|
+
end
|
25
|
+
|
26
|
+
When /^a client makes a (buggy )?load-balanced request to '(.*)'$/ do |buggy, path|
|
27
|
+
t = RightDevelop::Net::HTTPClient::DEFAULT_OPTIONS[:timeout]
|
28
|
+
o = RightDevelop::Net::HTTPClient::DEFAULT_OPTIONS[:open_timeout]
|
29
|
+
step "a client makes a #{buggy}load-balanced request to '#{path}' with timeout #{t} and open_timeout #{o}"
|
30
|
+
end
|
31
|
+
|
32
|
+
When /^a client makes a (buggy )?load-balanced request to '(.*)' with timeout (\d+) and open_timeout (\d+)$/ do |buggy, path, timeout, open_timeout|
|
33
|
+
buggy = !(buggy.nil? || buggy.empty?)
|
34
|
+
|
35
|
+
@mock_servers.should_not be_nil
|
36
|
+
@mock_servers.empty?.should be_false
|
37
|
+
|
38
|
+
timeout = timeout.to_i
|
39
|
+
open_timeout = open_timeout.to_i
|
40
|
+
urls = @mock_servers.map { |s| s.url }
|
41
|
+
@request_balancer = RightDevelop::Net::RequestBalancer.new(urls)
|
42
|
+
@request_attempts = 0
|
43
|
+
@request_t0 = Time.now
|
44
|
+
@http_client = RightDevelop::Net::HTTPClient.new
|
45
|
+
begin
|
46
|
+
@request_balancer.request do |url|
|
47
|
+
@request_attempts += 1
|
48
|
+
raise ArgumentError, "Fall down go boom!" if buggy
|
49
|
+
@http_client.get("#{url}#{path}", {:timeout => timeout, :open_timeout => open_timeout})
|
50
|
+
end
|
51
|
+
rescue Exception => e
|
52
|
+
@request_error = e
|
53
|
+
end
|
54
|
+
@request_t1 = Time.now
|
55
|
+
end
|
56
|
+
|
57
|
+
Then /^the request should (\w+ ?\w*)$/ do |behavior|
|
58
|
+
case behavior
|
59
|
+
when 'complete'
|
60
|
+
error_expected = false
|
61
|
+
when 'raise'
|
62
|
+
error_expected = true
|
63
|
+
when /raise (\w+)/
|
64
|
+
error_expected = true
|
65
|
+
error_class_expected = /raise (\w+)/.match(behavior)[1]
|
66
|
+
else
|
67
|
+
raise ArgumentError, "Unknown request behavior #{behavior}"
|
68
|
+
end
|
69
|
+
|
70
|
+
if !error_expected && @request_error
|
71
|
+
puts '!' * 80
|
72
|
+
puts @request_error.class.inspect
|
73
|
+
puts @request_error.class.superclass.inspect
|
74
|
+
puts '!' * 80
|
75
|
+
end
|
76
|
+
@request_error.should be_nil unless error_expected
|
77
|
+
@request_error.should_not be_nil if error_expected
|
78
|
+
@request_error.class.name.should =~ Regexp.new(error_class_expected) if error_class_expected
|
79
|
+
end
|
80
|
+
|
81
|
+
Then /^the request should (\w+ ?\w*) in less than (\d+) seconds?$/ do |behavior, time|
|
82
|
+
step "the request should #{behavior}"
|
83
|
+
#allow 10% margin of error due to Ruby/OS scheduler variance
|
84
|
+
(@request_t1.to_f - @request_t0.to_f).should <= (time.to_f * 1.10)
|
85
|
+
end
|
86
|
+
|
87
|
+
Then /^the request should be attempted once$/ do
|
88
|
+
@request_attempts.should == 1
|
89
|
+
end
|
90
|
+
|
91
|
+
Then /^the request should be attempted ([0-9]+) times$/ do |n|
|
92
|
+
@request_attempts.should == n.to_i
|
93
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
Given /^a Ruby application$/ do
|
4
|
+
ruby_app_root.should_not be_nil
|
5
|
+
end
|
6
|
+
|
7
|
+
Given /^a Gemfile$/ do
|
8
|
+
gemfile = ruby_app_path('Gemfile')
|
9
|
+
unless File.exist?(gemfile)
|
10
|
+
basedir = File.expand_path('../../..', __FILE__)
|
11
|
+
File.open(gemfile, 'w') do |file|
|
12
|
+
file.puts "gem 'right_develop', :path=>'#{basedir}'"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Given /^a gem dependency on '(.*)'$/ do |dependency|
|
18
|
+
step 'a Gemfile'
|
19
|
+
gem, version = dependency.split(/\s+/, 2)
|
20
|
+
gemfile = ruby_app_path('Gemfile')
|
21
|
+
File.open(gemfile, 'a') do |file|
|
22
|
+
file.puts "gem '#{gem}', '#{version}'"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Given /^a Rakefile$/ do
|
27
|
+
rakefile = ruby_app_path('Rakefile')
|
28
|
+
unless File.exist?(rakefile)
|
29
|
+
File.open(rakefile, 'w') do |file|
|
30
|
+
file.puts "# Auto-generated by #{__FILE__}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
Given /^the Rakefile contains a RightDevelop::CI::RakeTask$/ do
|
36
|
+
step 'a Rakefile'
|
37
|
+
rakefile = ruby_app_path('Rakefile')
|
38
|
+
File.open(rakefile, 'w') do |file|
|
39
|
+
file.puts "require 'right_develop'"
|
40
|
+
file.puts "RightDevelop::CI::RakeTask.new"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Given /^the Rakefile contains a RightDevelop::CI::RakeTask with parameter '(.*)'$/ do |ns|
|
45
|
+
step 'a Rakefile'
|
46
|
+
rakefile = ruby_app_path('Rakefile')
|
47
|
+
File.open(rakefile, 'w') do |file|
|
48
|
+
file.puts "require 'right_develop'"
|
49
|
+
file.puts "RightDevelop::CI::RakeTask.new(#{ns})"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Given /^the Rakefile contains:$/ do |content|
|
54
|
+
step 'a Rakefile'
|
55
|
+
rakefile = ruby_app_path('Rakefile')
|
56
|
+
File.open(rakefile, 'w') do |file|
|
57
|
+
file.puts "require 'right_develop'"
|
58
|
+
content.split("\n").each do |line|
|
59
|
+
file.puts line
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Given /^a trivial (failing )?RSpec spec$/ do |failing|
|
65
|
+
spec_dir = ruby_app_path('spec')
|
66
|
+
spec = ruby_app_path('spec', 'trivial_spec.rb')
|
67
|
+
FileUtils.mkdir_p(spec_dir)
|
68
|
+
File.open(spec, 'w') do |file|
|
69
|
+
file.puts "describe String do"
|
70
|
+
file.puts " it 'has a size' do"
|
71
|
+
file.puts " 'joe'.size.should == 3"
|
72
|
+
file.puts " end"
|
73
|
+
file.puts
|
74
|
+
file.puts " it 'is stringy' do"
|
75
|
+
file.puts " pending"
|
76
|
+
file.puts " end"
|
77
|
+
unless failing.nil?
|
78
|
+
file.puts
|
79
|
+
file.puts "it 'meets an impossible ideal' do"
|
80
|
+
file.puts " raise NotImplementedError, 'inconceivable!'"
|
81
|
+
file.puts "end"
|
82
|
+
end
|
83
|
+
file.puts "end"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
Given /^an RSpec spec named '([A-Za-z0-9_.]+)' with content:$/ do |name, content|
|
88
|
+
spec_dir = ruby_app_path('spec')
|
89
|
+
spec = ruby_app_path('spec', name)
|
90
|
+
FileUtils.mkdir_p(spec_dir)
|
91
|
+
File.open(spec, 'w') do |file|
|
92
|
+
content.split("\n").each do |line|
|
93
|
+
file.puts line
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
Given /^a trivial (failing )?Cucumber feature$/ do |failing|
|
99
|
+
features_dir = ruby_app_path('features')
|
100
|
+
steps_dir = ruby_app_path('features', 'step_definitions')
|
101
|
+
feature = ruby_app_path('features', 'trivial.feature')
|
102
|
+
steps = ruby_app_path('features', 'step_definitions', 'trivial_steps.rb')
|
103
|
+
FileUtils.mkdir_p(features_dir)
|
104
|
+
FileUtils.mkdir_p(steps_dir)
|
105
|
+
|
106
|
+
unless File.exist?(steps)
|
107
|
+
File.open(steps, 'w') do |file|
|
108
|
+
file.puts "When /^the night has come and the land is dark$/ do; end"
|
109
|
+
file.puts "When /^the moon is the only light we see$/ do; end"
|
110
|
+
file.puts "Then /^I won't be afraid.*$/ do; end"
|
111
|
+
file.puts "Then /^as long as you stand.*by me$/ do; end"
|
112
|
+
file.puts "Then /^you run away as fast as you can$/ do; raise NotImplementedError; end"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
unless File.exist?(feature)
|
116
|
+
File.open(feature, 'w') do |file|
|
117
|
+
file.puts "Feature: Song Lyrics from the 1950s"
|
118
|
+
file.puts
|
119
|
+
file.puts " Scenario: Stand By Me"
|
120
|
+
file.puts " When the night has come and the land is dark"
|
121
|
+
file.puts " And the moon is the only light we see"
|
122
|
+
file.puts " Then I won't be afraid, oh, I won't be afraid"
|
123
|
+
if failing.nil?
|
124
|
+
file.puts " And as long as you stand, stand by me"
|
125
|
+
else
|
126
|
+
file.puts " And you run away as fast as you can"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
When /^I install the bundle$/ do
|
133
|
+
ruby_app_shell('bundle install')
|
134
|
+
end
|
135
|
+
|
136
|
+
When /^I rake '(.*)'$/ do |task|
|
137
|
+
@ruby_app_output = ruby_app_shell("bundle exec rake #{task} --trace ", :ignore_errors => true)
|
138
|
+
end
|
139
|
+
|
140
|
+
When /^I debug the app shell$/ do
|
141
|
+
STDOUT.puts "Opening shell in a separate window."
|
142
|
+
if RUBY_PLATFORM =~ /darwin/
|
143
|
+
ruby_app_shell("open -a Terminal .")
|
144
|
+
else
|
145
|
+
raise "Don't know how to open an app shell for #{RUBY_PLATFORM}; please contribute your knowledge to #{__FILE__}"
|
146
|
+
end
|
147
|
+
STDOUT.puts "Press Enter to continue Cucumber execution..."
|
148
|
+
STDOUT.flush
|
149
|
+
STDIN.readline
|
150
|
+
end
|
151
|
+
|
152
|
+
Then /^the output should contain '(.*)'$/ do |expected_output|
|
153
|
+
@ruby_app_output.should include(expected_output)
|
154
|
+
end
|
155
|
+
|
156
|
+
Then /^the output should not contain '(.*)'$/ do |expected_output|
|
157
|
+
@ruby_app_output.should_not include(expected_output)
|
158
|
+
end
|
159
|
+
|
160
|
+
Then /^the directory '(.*)' should contain files/ do |dir|
|
161
|
+
dir = ruby_app_path(dir)
|
162
|
+
File.directory?(dir).should be_true
|
163
|
+
Dir[File.join(dir, '*')].should_not be_empty
|
164
|
+
end
|
165
|
+
|
166
|
+
Then /^the file '(.*)' should (not )?exist?$/ do |file, negatory|
|
167
|
+
file = ruby_app_path(file)
|
168
|
+
|
169
|
+
if negatory.nil? || negatory.empty?
|
170
|
+
Pathname.new(file).should exist
|
171
|
+
else
|
172
|
+
Pathname.new(file).should_not exist
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
Then /^the file '(.*)' should mention ([0-9]) (passing|failing) test cases?$/ do |file, n, pass_fail|
|
177
|
+
file = ruby_app_path(file)
|
178
|
+
n = Integer(n)
|
179
|
+
|
180
|
+
Pathname.new(file).should exist
|
181
|
+
|
182
|
+
doc = Nokogiri.XML(File.open(file, 'r'))
|
183
|
+
|
184
|
+
all_testcases = doc.css('testcase').size
|
185
|
+
failing_testcases = doc.css('testcase failure').size
|
186
|
+
passing_testcases = all_testcases - failing_testcases
|
187
|
+
|
188
|
+
case pass_fail
|
189
|
+
when 'passing'
|
190
|
+
passing_testcases.should == n
|
191
|
+
when 'failing'
|
192
|
+
failing_testcases.should == n
|
193
|
+
else
|
194
|
+
raise NotImplementedError, "WTF #{pass_fail}"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
Then /^the command should (succeed|fail)$/ do |success|
|
199
|
+
if success == 'succeed'
|
200
|
+
$?.exitstatus.should == 0
|
201
|
+
elsif success == 'fail'
|
202
|
+
$?.exitstatus.should_not == 0
|
203
|
+
else
|
204
|
+
raise NotImplementedError, "Unknown expectation #{success}"
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
Given /^a serializer named '(.+)'$/ do |klass|
|
2
|
+
@serializer = klass.to_const
|
3
|
+
end
|
4
|
+
|
5
|
+
Given /^a stateful Ruby class named '(.*)'$/ do |name|
|
6
|
+
name = name.to_sym
|
7
|
+
ivars = 1 + rand(10)
|
8
|
+
|
9
|
+
|
10
|
+
Kernel.__send__(:remove_const, name) if Kernel.const_defined?(name)
|
11
|
+
|
12
|
+
@stateful_ruby_class = Class.new(Object)
|
13
|
+
Kernel.__send__(:const_set, name, @stateful_ruby_class)
|
14
|
+
|
15
|
+
@stateful_ruby_class.instance_eval do
|
16
|
+
define_method(:initialize) do
|
17
|
+
ivars.times do
|
18
|
+
self.instance_variable_set("@instance_variable_#{random_value(Integer)}", random_value(nil, [String]))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
define_method(:==) do |other|
|
23
|
+
result = (Set.new(self.instance_variables) == Set.new(other.instance_variables))
|
24
|
+
|
25
|
+
self.instance_variables.each do |ivar|
|
26
|
+
result &&= (self.instance_variable_get(ivar) == other.instance_variable_get(ivar))
|
27
|
+
end
|
28
|
+
|
29
|
+
result
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
When /^I serialize the Ruby value: (.*)$/ do |expression|
|
35
|
+
@ruby_value = eval(expression)
|
36
|
+
@serialized_value = @serializer.dump(@ruby_value)
|
37
|
+
end
|
38
|
+
|
39
|
+
When /^I serialize a complex random data structure$/ do
|
40
|
+
@ruby_value = random_value(nil, [String])
|
41
|
+
@serialized_value = @serializer.dump(@ruby_value)
|
42
|
+
end
|
43
|
+
|
44
|
+
When /^an eldritch force deletes a key from the serialized value$/ do
|
45
|
+
hash = RightDevelop::Data::Serializer::Encoder.load(@serialized_value)
|
46
|
+
hash.delete(hash.keys[rand(hash.keys.size)])
|
47
|
+
@serialized_value = @serializer.dump(hash)
|
48
|
+
end
|
49
|
+
|
50
|
+
Then /^the serialized value should be: (.*)$/ do |expression|
|
51
|
+
if (@serialized_value =~ /^\{/) || (expression =~ /^\{/)
|
52
|
+
# Hash: ordering of JSON representation is unimportant; load as pure JSON and compare values
|
53
|
+
RightDevelop::Data::Serializer::Encoder.load(@serialized_value).should ==
|
54
|
+
RightDevelop::Data::Serializer::Encoder.load(expression)
|
55
|
+
else
|
56
|
+
# Any other data: exact comparison
|
57
|
+
@serialized_value.should == expression
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Then /^the serialized value should round-trip cleanly$/ do
|
62
|
+
case @ruby_value
|
63
|
+
when Float
|
64
|
+
# Floating-point numbers lose some precision due to truncation
|
65
|
+
@serializer.load(@serialized_value).should be_within(0.000001).of(@ruby_value)
|
66
|
+
when Time
|
67
|
+
# Times are stored with accuracy ~ 1 sec
|
68
|
+
@serializer.load(@serialized_value).to_i.should == @ruby_value.to_i
|
69
|
+
else
|
70
|
+
# Everything else should compare identical
|
71
|
+
@serializer.load(@serialized_value).should == @ruby_value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
When /^the serialized value should fail to round\-trip$/ do
|
76
|
+
@serializer.load(@serialized_value).should_not == @ruby_value
|
77
|
+
end
|
78
|
+
|
79
|
+
Then /^the serialized value should be a JSON (.*)$/ do |json_type|
|
80
|
+
case json_type
|
81
|
+
when 'object'
|
82
|
+
@serialized_value.should =~ /^\{.*\}$/
|
83
|
+
when 'string'
|
84
|
+
@serialized_value.should =~ /^".*"$/
|
85
|
+
when 'number'
|
86
|
+
@serialized_value.should =~ /^".*"$/
|
87
|
+
when 'true', 'false', 'null'
|
88
|
+
@serialized_value.should == json_type
|
89
|
+
else
|
90
|
+
raise NotImplementedError, "Unknown JSON type"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
Then /^the serialized value should have a suitable _ruby_class/ do
|
95
|
+
@serialized_value.should =~ /"_ruby_class":"#{Regexp.escape(@ruby_value.class.name)}"/
|
96
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
#-- -*- mode: ruby; encoding: utf-8 -*-
|
2
|
+
# Copyright: Copyright (c) 2011 RightScale, Inc.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# 'Software'), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following 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 OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
|
24
|
+
require 'webrick'
|
25
|
+
require 'webrick/httpservlet'
|
26
|
+
|
27
|
+
# Mimic a generic HTTP server with various configurable behaviors
|
28
|
+
class MockServer < WEBrick::HTTPServer
|
29
|
+
attr_accessor :thread, :port, :url
|
30
|
+
|
31
|
+
# Overridden factory method that will tolerate Errno::EADDRINUSE in case a certain
|
32
|
+
# TCP port is already used
|
33
|
+
def self.new(*args)
|
34
|
+
tries ||= 0
|
35
|
+
super
|
36
|
+
rescue Errno::EADDRINUSE => e
|
37
|
+
tries += 1
|
38
|
+
if tries > 5
|
39
|
+
raise
|
40
|
+
else
|
41
|
+
retry
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(options={})
|
46
|
+
@port = options[:port] || (4096 + rand(32768-4096))
|
47
|
+
@url = "http://localhost:#{@port}"
|
48
|
+
|
49
|
+
logger = WEBrick::Log.new(STDERR, WEBrick::Log::FATAL)
|
50
|
+
super(options.merge(:Port => @port, :AccessLog => [], :Logger=>logger))
|
51
|
+
|
52
|
+
# mount servlets via callback
|
53
|
+
yield(self)
|
54
|
+
|
55
|
+
#Start listening for HTTP in a separate thread
|
56
|
+
@thread = Thread.new do
|
57
|
+
self.start()
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Mimic a server that exists but hangs without providing any response, not even
|
63
|
+
# ICMP signals e.g. host unreachable or network unreachable. We simulate this
|
64
|
+
# by pointing at a port of the RightScale (my) load balancer that is not allowed
|
65
|
+
# by the security group, which causes the TCP SYN packets to be dropped with no
|
66
|
+
# acknowledgement.
|
67
|
+
class BlackholedServer
|
68
|
+
attr_accessor :port, :url
|
69
|
+
|
70
|
+
def initialize(options={})
|
71
|
+
@port = options[:port] || (4096 + rand(4096))
|
72
|
+
@url = "my.rightscale.com:#{@port}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
Before do
|
77
|
+
@mock_servers = []
|
78
|
+
end
|
79
|
+
|
80
|
+
# Kill running reposes after test finishes.
|
81
|
+
After do
|
82
|
+
@mock_servers.each do |server|
|
83
|
+
if server.is_a?(MockServer)
|
84
|
+
server.thread.kill
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
Given /^(an?|\d+)? (overloaded|blackholed) servers?$/ do |number, behavior|
|
90
|
+
number = 0 if number =~ /no/
|
91
|
+
number = 1 if number =~ /an?/
|
92
|
+
number = number.to_i
|
93
|
+
|
94
|
+
number.times do
|
95
|
+
case behavior
|
96
|
+
when 'overloaded'
|
97
|
+
proc = Proc.new do
|
98
|
+
sleep(10)
|
99
|
+
'Hi there! I am overloaded.'
|
100
|
+
end
|
101
|
+
server = MockServer.new do |s|
|
102
|
+
s.mount('/', WEBrick::HTTPServlet::ProcHandler.new(proc))
|
103
|
+
end
|
104
|
+
when 'blackholed'
|
105
|
+
server = BlackholedServer.new()
|
106
|
+
else
|
107
|
+
raise ArgumentError, "Unknown server behavior #{behavior}"
|
108
|
+
end
|
109
|
+
|
110
|
+
@mock_servers << server
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
Given /^(an?|\d+)? servers? that responds? with ([0-9]+)$/ do |number, status_code|
|
115
|
+
number = 0 if number =~ /no/
|
116
|
+
number = 1 if number =~ /an?/
|
117
|
+
number = number.to_i
|
118
|
+
|
119
|
+
status_code = status_code.to_i
|
120
|
+
|
121
|
+
proc = Proc.new do
|
122
|
+
klass = WEBrick::HTTPStatus::CodeToError[status_code]
|
123
|
+
klass.should_not be_nil
|
124
|
+
raise klass, "Simulated #{status_code} response"
|
125
|
+
end
|
126
|
+
|
127
|
+
number.times do
|
128
|
+
server = MockServer.new do |s|
|
129
|
+
s.mount('/', WEBrick::HTTPServlet::ProcHandler.new(proc))
|
130
|
+
end
|
131
|
+
|
132
|
+
@mock_servers << server
|
133
|
+
end
|
134
|
+
end
|