right_support 2.6.17 → 2.7.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 +37 -0
- data/Gemfile +29 -0
- data/Gemfile.lock +111 -0
- data/README.rdoc +2 -0
- data/Rakefile +62 -0
- data/VERSION +1 -0
- data/features/balancer_error_handling.feature +34 -0
- data/features/balancer_health_check.feature +33 -0
- data/features/continuous_integration.feature +51 -0
- data/features/continuous_integration_cucumber.feature +28 -0
- data/features/continuous_integration_rspec1.feature +28 -0
- data/features/continuous_integration_rspec2.feature +28 -0
- data/features/http_client_timeout.feature +19 -0
- data/features/serialization.feature +95 -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 +176 -0
- data/features/step_definitions/serialization_steps.rb +96 -0
- data/features/step_definitions/server_steps.rb +134 -0
- data/features/support/env.rb +138 -0
- data/features/support/file_utils_bundler_mixin.rb +45 -0
- data/lib/right_support/ci/java_cucumber_formatter.rb +22 -8
- data/lib/right_support/ci/java_spec_formatter.rb +26 -8
- data/lib/right_support/ci/rake_task.rb +3 -0
- data/lib/right_support/ci.rb +24 -0
- data/lib/right_support/crypto/signed_hash.rb +22 -0
- data/lib/right_support/data/serializer.rb +24 -2
- data/lib/right_support/net/address_helper.rb +20 -8
- data/lib/right_support/net/dns.rb +20 -8
- data/lib/right_support/net/http_client.rb +22 -0
- data/lib/right_support/net/request_balancer.rb +27 -21
- data/lib/right_support/net/s3_helper.rb +20 -8
- data/lib/right_support/net/ssl/open_ssl_patch.rb +22 -0
- data/lib/right_support/net/ssl.rb +20 -8
- data/lib/right_support/ruby/easy_singleton.rb +22 -0
- data/lib/right_support/ruby/object_extensions.rb +22 -0
- data/lib/right_support/ruby/string_extensions.rb +1 -1
- data/lib/right_support.rb +13 -10
- data/right_support.gemspec +180 -18
- data/right_support.rconf +8 -0
- data/spec/config/feature_set_spec.rb +83 -0
- data/spec/crypto/signed_hash_spec.rb +60 -0
- data/spec/data/hash_tools_spec.rb +471 -0
- data/spec/data/uuid_spec.rb +45 -0
- data/spec/db/cassandra_model_part1_spec.rb +84 -0
- data/spec/db/cassandra_model_part2_spec.rb +73 -0
- data/spec/db/cassandra_model_spec.rb +359 -0
- data/spec/fixtures/encrypted_priv_rsa.pem +30 -0
- data/spec/fixtures/good_priv_dsa.pem +12 -0
- data/spec/fixtures/good_priv_rsa.pem +15 -0
- data/spec/fixtures/good_pub_dsa.ssh +1 -0
- data/spec/fixtures/good_pub_rsa.pem +5 -0
- data/spec/fixtures/good_pub_rsa.ssh +1 -0
- data/spec/log/exception_logger_spec.rb +76 -0
- data/spec/log/filter_logger_spec.rb +8 -0
- data/spec/log/mixin_spec.rb +62 -0
- data/spec/log/multiplexer_spec.rb +54 -0
- data/spec/log/null_logger_spec.rb +36 -0
- data/spec/log/system_logger_spec.rb +92 -0
- data/spec/net/address_helper_spec.rb +57 -0
- data/spec/net/balancing/health_check_spec.rb +382 -0
- data/spec/net/balancing/round_robin_spec.rb +15 -0
- data/spec/net/balancing/sticky_policy_spec.rb +92 -0
- data/spec/net/dns_spec.rb +152 -0
- data/spec/net/http_client_spec.rb +171 -0
- data/spec/net/request_balancer_spec.rb +579 -0
- data/spec/net/s3_helper_spec.rb +160 -0
- data/spec/net/ssl_spec.rb +42 -0
- data/spec/net/string_encoder_spec.rb +58 -0
- data/spec/rack/log_setter_spec.rb +5 -0
- data/spec/rack/request_logger_spec.rb +68 -0
- data/spec/rack/request_tracker_spec.rb +5 -0
- data/spec/ruby/easy_singleton_spec.rb +72 -0
- data/spec/ruby/object_extensions_spec.rb +27 -0
- data/spec/ruby/string_extensions_spec.rb +98 -0
- data/spec/spec_helper.rb +181 -0
- data/spec/stats/activity_spec.rb +193 -0
- data/spec/stats/exceptions_spec.rb +123 -0
- data/spec/stats/helpers_spec.rb +603 -0
- data/spec/validation/openssl_spec.rb +37 -0
- data/spec/validation/ssh_spec.rb +39 -0
- metadata +218 -19
@@ -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
|
+
RightSupport::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("RightSupport::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 = RightSupport::Net::HTTPClient::DEFAULT_OPTIONS[:timeout]
|
28
|
+
o = RightSupport::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 = RightSupport::Net::RequestBalancer.new(urls)
|
42
|
+
@request_attempts = 0
|
43
|
+
@request_t0 = Time.now
|
44
|
+
@http_client = RightSupport::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,176 @@
|
|
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_support', :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 RightSupport::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_support/ci/rake_task'"
|
40
|
+
file.puts "RightSupport::CI::RakeTask.new"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
Given /^the Rakefile contains a RightSupport::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_support/ci/rake_task'"
|
49
|
+
file.puts "RightSupport::CI::RakeTask.new(#{ns})"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Given /^a trivial (failing )?RSpec spec$/ do |failing|
|
54
|
+
spec_dir = ruby_app_path('spec')
|
55
|
+
spec = ruby_app_path('spec', 'trivial_spec.rb')
|
56
|
+
FileUtils.mkdir_p(spec_dir)
|
57
|
+
unless File.exist?(spec)
|
58
|
+
File.open(spec, 'w') do |file|
|
59
|
+
file.puts "describe String do"
|
60
|
+
file.puts " it 'has a size' do"
|
61
|
+
file.puts " 'joe'.size.should == 3"
|
62
|
+
file.puts " end"
|
63
|
+
file.puts
|
64
|
+
file.puts " it 'is stringy' do"
|
65
|
+
file.puts " pending"
|
66
|
+
file.puts " end"
|
67
|
+
unless failing.nil?
|
68
|
+
file.puts
|
69
|
+
file.puts "it 'meets an impossible ideal' do"
|
70
|
+
file.puts " raise NotImplementedError, 'inconceivable!'"
|
71
|
+
file.puts "end"
|
72
|
+
end
|
73
|
+
file.puts "end"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
Given /^a trivial (failing )?Cucumber feature$/ do |failing|
|
79
|
+
features_dir = ruby_app_path('features')
|
80
|
+
steps_dir = ruby_app_path('features', 'step_definitions')
|
81
|
+
feature = ruby_app_path('features', 'trivial.feature')
|
82
|
+
steps = ruby_app_path('features', 'step_definitions', 'trivial_steps.rb')
|
83
|
+
FileUtils.mkdir_p(features_dir)
|
84
|
+
FileUtils.mkdir_p(steps_dir)
|
85
|
+
|
86
|
+
unless File.exist?(steps)
|
87
|
+
File.open(steps, 'w') do |file|
|
88
|
+
file.puts "When /^the night has come and the land is dark$/ do; end"
|
89
|
+
file.puts "When /^the moon is the only light we see$/ do; end"
|
90
|
+
file.puts "Then /^I won't be afraid.*$/ do; end"
|
91
|
+
file.puts "Then /^as long as you stand.*by me$/ do; end"
|
92
|
+
file.puts "Then /^you run away as fast as you can$/ do; raise NotImplementedError; end"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
unless File.exist?(feature)
|
96
|
+
File.open(feature, 'w') do |file|
|
97
|
+
file.puts "Feature: Song Lyrics from the 1950s"
|
98
|
+
file.puts
|
99
|
+
file.puts " Scenario: Stand By Me"
|
100
|
+
file.puts " When the night has come and the land is dark"
|
101
|
+
file.puts " And the moon is the only light we see"
|
102
|
+
file.puts " Then I won't be afraid, oh, I won't be afraid"
|
103
|
+
if failing.nil?
|
104
|
+
file.puts " And as long as you stand, stand by me"
|
105
|
+
else
|
106
|
+
file.puts " And you run away as fast as you can"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
When /^I install the bundle$/ do
|
113
|
+
ruby_app_shell('bundle install')
|
114
|
+
end
|
115
|
+
|
116
|
+
When /^I rake '(.*)'$/ do |task|
|
117
|
+
@ruby_app_output = ruby_app_shell("bundle exec rake #{task} --trace ", :ignore_errors => true)
|
118
|
+
end
|
119
|
+
|
120
|
+
When /^I debug the app shell$/ do
|
121
|
+
STDOUT.puts "Opening shell in a separate window."
|
122
|
+
if RUBY_PLATFORM =~ /darwin/
|
123
|
+
ruby_app_shell("open -a Terminal .")
|
124
|
+
else
|
125
|
+
raise "Don't know how to open an app shell for #{RUBY_PLATFORM}; please contribute your knowledge to #{__FILE__}"
|
126
|
+
end
|
127
|
+
STDOUT.puts "Press Enter to continue Cucumber execution..."
|
128
|
+
STDOUT.flush
|
129
|
+
STDIN.readline
|
130
|
+
end
|
131
|
+
|
132
|
+
Then /^the output should contain '(.*)'$/ do |expected_output|
|
133
|
+
@ruby_app_output.should include(expected_output)
|
134
|
+
end
|
135
|
+
|
136
|
+
Then /^the output should not contain '(.*)'$/ do |expected_output|
|
137
|
+
@ruby_app_output.should_not include(expected_output)
|
138
|
+
end
|
139
|
+
|
140
|
+
Then /^the directory '(.*)' should contain files/ do |dir|
|
141
|
+
dir = ruby_app_path(dir)
|
142
|
+
File.directory?(dir).should be_true
|
143
|
+
Dir[File.join(dir, '*')].should_not be_empty
|
144
|
+
end
|
145
|
+
|
146
|
+
Then /^the file '(.*)' should mention ([0-9]) (passing|failing) test cases?$/ do |file, n, pass_fail|
|
147
|
+
file = ruby_app_path(file)
|
148
|
+
n = Integer(n)
|
149
|
+
|
150
|
+
Pathname.new(file).should exist
|
151
|
+
|
152
|
+
doc = Nokogiri.XML(File.open(file, 'r'))
|
153
|
+
|
154
|
+
all_testcases = doc.css('testcase').size
|
155
|
+
failing_testcases = doc.css('testcase failure').size
|
156
|
+
passing_testcases = all_testcases - failing_testcases
|
157
|
+
|
158
|
+
case pass_fail
|
159
|
+
when 'passing'
|
160
|
+
passing_testcases.should == n
|
161
|
+
when 'failing'
|
162
|
+
failing_testcases.should == n
|
163
|
+
else
|
164
|
+
raise NotImplementedError, "WTF #{pass_fail}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
Then /^the command should (succeed|fail)$/ do |success|
|
169
|
+
if success == 'succeed'
|
170
|
+
$?.exitstatus.should == 0
|
171
|
+
elsif success == 'fail'
|
172
|
+
$?.exitstatus.should_not == 0
|
173
|
+
else
|
174
|
+
raise NotImplementedError, "Unknown expectation #{success}"
|
175
|
+
end
|
176
|
+
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 = RightSupport::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
|
+
RightSupport::Data::Serializer::Encoder.load(@serialized_value).should ==
|
54
|
+
RightSupport::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
|
@@ -0,0 +1,138 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright: Copyright (c) 2010- 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 'tmpdir'
|
25
|
+
|
26
|
+
require 'rubygems'
|
27
|
+
require 'bundler/setup'
|
28
|
+
|
29
|
+
$:.unshift(File.expand_path('../../../lib', __FILE__)) #ensure we load THIS project's code, not an installed gem
|
30
|
+
require 'right_support'
|
31
|
+
require 'right_support/ci'
|
32
|
+
|
33
|
+
module RandomValueHelper
|
34
|
+
RANDOM_KEY_CLASSES = [String, Integer, Float, TrueClass, FalseClass]
|
35
|
+
RANDOM_VALUE_CLASSES = RANDOM_KEY_CLASSES + [Array, Hash]
|
36
|
+
|
37
|
+
def random_value(klass=nil, key_classes=RANDOM_KEY_CLASSES, depth=0)
|
38
|
+
if klass.nil?
|
39
|
+
if depth < 3 && key_classes.nil?
|
40
|
+
klasses = RANDOM_VALUE_CLASSES
|
41
|
+
else
|
42
|
+
klasses = key_classes
|
43
|
+
end
|
44
|
+
|
45
|
+
klass = klasses[rand(klasses.size)]
|
46
|
+
end
|
47
|
+
|
48
|
+
if klass == String
|
49
|
+
result = ''
|
50
|
+
io = StringIO.new(result, 'w')
|
51
|
+
rand(40).times { io.write(0x61 + rand(26)) }
|
52
|
+
io.close
|
53
|
+
elsif klass == Integer
|
54
|
+
result = rand(0xffffff)
|
55
|
+
elsif klass == Float
|
56
|
+
result = rand(0xffffff) * rand
|
57
|
+
elsif klass == TrueClass
|
58
|
+
result = true
|
59
|
+
elsif klass == FalseClass
|
60
|
+
result = false
|
61
|
+
elsif klass == Array
|
62
|
+
result = []
|
63
|
+
rand(10).times { result << random_value(nil, key_classes,depth+1) }
|
64
|
+
elsif klass == Hash
|
65
|
+
result = {}
|
66
|
+
if string_keys
|
67
|
+
key_type = String
|
68
|
+
else
|
69
|
+
key_type = RANDOM_KEY_CLASSES[rand(RANDOM_KEY_CLASSES.size)]
|
70
|
+
end
|
71
|
+
rand(10).times { result[random_value(key_type, key_classes, depth+1)] = random_value(nil, key_classes, depth+1) }
|
72
|
+
else
|
73
|
+
raise ArgumentError, "Unknown random value type #{klass}"
|
74
|
+
end
|
75
|
+
|
76
|
+
result
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module RubyAppHelper
|
81
|
+
def ruby_app_root
|
82
|
+
@ruby_app_root ||= Dir.mktmpdir('right_support_cucumber_ruby')
|
83
|
+
end
|
84
|
+
|
85
|
+
def ruby_app_path(*args)
|
86
|
+
path = ruby_app_root
|
87
|
+
until args.empty?
|
88
|
+
item = args.shift
|
89
|
+
path = File.join(path, item)
|
90
|
+
end
|
91
|
+
path
|
92
|
+
end
|
93
|
+
|
94
|
+
# Run a shell command in app_dir, e.g. a rake task
|
95
|
+
def ruby_app_shell(cmd, options={})
|
96
|
+
ignore_errors = options[:ignore_errors] || false
|
97
|
+
log = !!(Cucumber.logger)
|
98
|
+
|
99
|
+
all_output = ''
|
100
|
+
Dir.chdir(ruby_app_root) do
|
101
|
+
Cucumber.logger.debug("bash> #{cmd}\n") if log
|
102
|
+
Bundler.with_clean_env do
|
103
|
+
IO.popen("#{cmd} 2>&1", 'r') do |output|
|
104
|
+
output.sync = true
|
105
|
+
done = false
|
106
|
+
until done
|
107
|
+
begin
|
108
|
+
line = output.readline + "\n"
|
109
|
+
all_output << line
|
110
|
+
Cucumber.logger.debug(line) if log
|
111
|
+
rescue EOFError
|
112
|
+
done = true
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
$?.success?.should(be_true) unless ignore_errors
|
120
|
+
all_output
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
module RightSupportWorld
|
125
|
+
include RandomValueHelper
|
126
|
+
include RubyAppHelper
|
127
|
+
end
|
128
|
+
|
129
|
+
# The Cucumber world
|
130
|
+
World(RightSupportWorld)
|
131
|
+
|
132
|
+
After do
|
133
|
+
FileUtils.rm_rf(ruby_app_root) if File.directory?(ruby_app_root)
|
134
|
+
end
|
135
|
+
|
136
|
+
class Object
|
137
|
+
include RandomValueHelper
|
138
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Copyright (c) 2012- RightScale Inc
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
# Magic hooks that is necessary due to Bundler/RVM craziness interfering with
|
23
|
+
# our own Cucumbers. This is necessary because our Cucumber scenarios need to create a
|
24
|
+
# Ruby app that has a distinct, separate bundle from right_support's own Gemfile, e.g. we
|
25
|
+
# need to test what happens when RSpec or Cucumber isn't available. Therefore the subprocesses
|
26
|
+
# that RightSupport's Cucumber suite launches, should not inherit our bundle.
|
27
|
+
module FileUtilsBundlerMixin
|
28
|
+
def self.included(base)
|
29
|
+
if base.respond_to?(:sh) && !base.respond_to?(:sh_without_bundler_taint)
|
30
|
+
base.instance_eval {
|
31
|
+
alias_method :sh_without_bundler_taint, :sh
|
32
|
+
alias_method :sh, :sh_with_bundler_taint
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def sh_with_bundler_taint(*params)
|
38
|
+
Bundler.with_clean_env do
|
39
|
+
sh_without_bundler_taint(*params)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Install the magic hook.
|
45
|
+
Kernel.instance_eval { include(::FileUtilsBundlerMixin) }
|