nonnative 2.1.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -124,14 +124,10 @@ module Nonnative
124
124
  processes = cfg.processes || []
125
125
  processes.each do |fd|
126
126
  process do |d|
127
- d.name = fd.name
128
127
  d.command = command(fd)
129
- d.timeout = fd.timeout
130
- d.wait = fd.wait if fd.wait
131
- d.port = fd.port
132
- d.log = fd.log
133
128
  d.signal = fd.signal
134
129
  d.environment = fd.environment
130
+ runner_attributes(d, fd)
135
131
 
136
132
  proxy d, fd.proxy
137
133
  end
@@ -154,12 +150,8 @@ module Nonnative
154
150
  servers = cfg.servers || []
155
151
  servers.each do |fd|
156
152
  server do |s|
157
- s.name = fd.name
158
153
  s.klass = Object.const_get(fd.class)
159
- s.timeout = fd.timeout
160
- s.wait = fd.wait if fd.wait
161
- s.port = fd.port
162
- s.log = fd.log
154
+ runner_attributes(s, fd)
163
155
 
164
156
  proxy s, fd.proxy
165
157
  end
@@ -179,6 +171,15 @@ module Nonnative
179
171
  end
180
172
  end
181
173
 
174
+ def runner_attributes(runner, loaded)
175
+ runner.name = loaded.name
176
+ runner.timeout = loaded.timeout
177
+ runner.wait = loaded.wait if loaded.wait
178
+ runner.host = loaded.host if loaded.host
179
+ runner.port = loaded.port
180
+ runner.log = loaded.log if loaded.respond_to?(:log)
181
+ end
182
+
182
183
  def proxy(runner, proxy)
183
184
  return unless proxy
184
185
 
@@ -20,7 +20,8 @@ module Nonnative
20
20
  # @return [String, nil] path to proxy log file (implementation-dependent)
21
21
  # @return [Numeric] wait interval (seconds) after proxy state changes (defaults to `0.1`)
22
22
  # @return [Hash] proxy implementation options (implementation-dependent)
23
- attr_accessor :kind, :host, :port, :log, :wait, :options
23
+ attr_accessor :kind, :host, :port, :log, :wait
24
+ attr_reader :options
24
25
 
25
26
  # Creates a proxy configuration with defaults.
26
27
  #
@@ -39,5 +40,16 @@ module Nonnative
39
40
  self.wait = 0.1
40
41
  self.options = {}
41
42
  end
43
+
44
+ # Stores proxy implementation options.
45
+ #
46
+ # Nil is normalized to an empty hash so callers loading partial configuration do not erase the
47
+ # default options container.
48
+ #
49
+ # @param value [Hash, nil]
50
+ # @return [void]
51
+ def options=(value)
52
+ @options = value || {}
53
+ end
42
54
  end
43
55
  end
@@ -1,105 +1,221 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- World(RSpec::Benchmark::Matchers)
4
- World(RSpec::Matchers)
5
- World(RSpec::Wait)
6
-
7
- Before('@startup') do
8
- Nonnative.start
9
- end
10
-
11
- After('@startup') do
12
- Nonnative.stop
13
- end
14
-
15
- After('@manual') do
16
- Nonnative.stop
17
- end
18
-
19
- Before('@clear') do
20
- Nonnative.clear
21
- end
22
-
23
- After('@reset') do
24
- Nonnative.reset
25
- end
26
-
27
- Given('I set the proxy for process {string} to {string}') do |name, operation|
28
- process = Nonnative.pool.process_by_name(name)
29
- process.proxy.send(operation)
30
- end
31
-
32
- Given('I set the proxy for server {string} to {string}') do |name, operation|
33
- server = Nonnative.pool.server_by_name(name)
34
- server.proxy.send(operation)
35
- end
36
-
37
- Given('I set the proxy for service {string} to {string}') do |name, operation|
38
- service = Nonnative.pool.service_by_name(name)
39
- service.proxy.send(operation)
40
- end
41
-
42
- Given('I start the system') do
43
- Nonnative.start
44
- end
45
-
46
- Given('I should see {string} as unhealthy') do |service|
47
- opts = {
48
- headers: { content_type: :json, accept: :json },
49
- read_timeout: 10, open_timeout: 10
50
- }
51
-
52
- wait_for { Nonnative.observability.health(opts).code }.to eq(503)
53
- wait_for { Nonnative.observability.health(opts).body }.to include(service)
54
- end
55
-
56
- Then('I should reset the proxy for process {string}') do |name|
57
- process = Nonnative.pool.process_by_name(name)
58
- process.proxy.reset
59
- end
60
-
61
- Then('I should reset the proxy for server {string}') do |name|
62
- server = Nonnative.pool.server_by_name(name)
63
- server.proxy.reset
64
- end
65
-
66
- Then('I should reset the proxy for service {string}') do |name|
67
- service = Nonnative.pool.service_by_name(name)
68
- service.proxy.reset
69
- end
70
-
71
- Then('the process {string} should consume less than {string} of memory') do |name, mem|
72
- process = Nonnative.pool.process_by_name(name)
73
- _, size, type = mem.split(/(\d+)/)
74
- actual = process.memory.send(type)
75
- size = size.to_i
76
-
77
- expect(actual).to be < size
78
- end
79
-
80
- Then('starting the system should raise an error') do
81
- expect { Nonnative.start }.to raise_error(Nonnative::StartError)
82
- end
83
-
84
- Then('stopping the system should raise an error') do
85
- expect { Nonnative.stop }.to raise_error(Nonnative::StopError)
86
- end
87
-
88
- Then('I should see a log entry of {string} for process {string}') do |message, process|
89
- process = Nonnative.configuration.process_by_name(process)
90
- expect(Nonnative.log_lines(process.log, ->(l) { l.include?(message) }).first).to include(message)
91
- end
92
-
93
- Then('I should see a log entry of {string} in the file {string}') do |message, path|
94
- expect(Nonnative.log_lines(path, ->(l) { l.include?(message) }).first).to include(message)
95
- end
96
-
97
- Then('I should see {string} as healthy') do |service|
98
- opts = {
99
- headers: { content_type: :json, accept: :json },
100
- read_timeout: 10, open_timeout: 10
101
- }
102
-
103
- wait_for { Nonnative.observability.health(opts).code }.to eq(200)
104
- wait_for { Nonnative.observability.health(opts).body }.to_not include(service)
105
- end
3
+ require 'cucumber'
4
+
5
+ module Nonnative
6
+ # Lazily installs the Cucumber integration once the Cucumber Ruby DSL is ready.
7
+ #
8
+ # Requiring `nonnative` outside a running Cucumber environment should not fail, but when Cucumber
9
+ # does finish booting its support-code registry this installer still needs to register the hooks
10
+ # and step definitions defined here.
11
+ module Cucumber
12
+ module LanguageHook
13
+ def rb_language=(value)
14
+ super.tap { ::Nonnative::Cucumber.install! }
15
+ end
16
+ end
17
+
18
+ module WorldHooks
19
+ def install_world
20
+ World(::RSpec::Benchmark::Matchers)
21
+ World(::RSpec::Matchers)
22
+ World(::RSpec::Wait)
23
+ end
24
+
25
+ def install_hooks
26
+ Before('@startup') { Nonnative.start }
27
+ After('@startup') { Nonnative.stop }
28
+ After('@manual') { Nonnative.stop }
29
+ Before('@clear') { Nonnative.clear }
30
+ After('@reset') { Nonnative.reset }
31
+ end
32
+ end
33
+
34
+ module ProxySteps
35
+ def install_proxy_steps
36
+ install_proxy_mutation_steps
37
+ install_proxy_reset_steps
38
+ end
39
+
40
+ def install_proxy_mutation_steps
41
+ Given('I set the proxy for process {string} to {string}') do |name, operation|
42
+ process = Nonnative.pool.process_by_name(name)
43
+ process.proxy.send(operation)
44
+ end
45
+
46
+ Given('I set the proxy for server {string} to {string}') do |name, operation|
47
+ server = Nonnative.pool.server_by_name(name)
48
+ server.proxy.send(operation)
49
+ end
50
+
51
+ Given('I set the proxy for service {string} to {string}') do |name, operation|
52
+ service = Nonnative.pool.service_by_name(name)
53
+ service.proxy.send(operation)
54
+ end
55
+ end
56
+
57
+ def install_proxy_reset_steps
58
+ Then('I should reset the proxy for process {string}') do |name|
59
+ process = Nonnative.pool.process_by_name(name)
60
+ process.proxy.reset
61
+ end
62
+
63
+ Then('I should reset the proxy for server {string}') do |name|
64
+ server = Nonnative.pool.server_by_name(name)
65
+ server.proxy.reset
66
+ end
67
+
68
+ Then('I should reset the proxy for service {string}') do |name|
69
+ service = Nonnative.pool.service_by_name(name)
70
+ service.proxy.reset
71
+ end
72
+ end
73
+ end
74
+
75
+ module LifecycleSteps
76
+ def install_state_steps
77
+ install_start_step
78
+ install_attempt_start_step
79
+ install_attempt_stop_step
80
+ install_unhealthy_step
81
+ install_healthy_step
82
+ end
83
+
84
+ def install_start_step
85
+ When('I start the system') do
86
+ Nonnative.start
87
+ end
88
+ end
89
+
90
+ def install_attempt_start_step
91
+ When('I attempt to start the system') do
92
+ @start_error = nil
93
+ Nonnative.start
94
+ rescue StandardError => e
95
+ @start_error = e
96
+ end
97
+ end
98
+
99
+ def install_attempt_stop_step
100
+ When('I attempt to stop the system') do
101
+ @stop_error = nil
102
+ Nonnative.stop
103
+ rescue StandardError => e
104
+ @stop_error = e
105
+ end
106
+ end
107
+
108
+ def install_unhealthy_step
109
+ opts = observability_options
110
+
111
+ Then('I should see {string} as unhealthy') do |service|
112
+ wait_for { Nonnative.observability.health(opts).code }.to eq(503)
113
+ wait_for { Nonnative.observability.health(opts).body }.to include(service)
114
+ end
115
+ end
116
+
117
+ def install_healthy_step
118
+ opts = observability_options
119
+
120
+ Then('I should see {string} as healthy') do |service|
121
+ wait_for { Nonnative.observability.health(opts).code }.to eq(200)
122
+ wait_for { Nonnative.observability.health(opts).body }.to_not include(service)
123
+ end
124
+ end
125
+
126
+ def observability_options
127
+ {
128
+ headers: { content_type: :json, accept: :json },
129
+ read_timeout: 10,
130
+ open_timeout: 10
131
+ }
132
+ end
133
+ end
134
+
135
+ module Assertions
136
+ def install_assertion_steps
137
+ install_memory_assertion_step
138
+ install_error_assertion_steps
139
+ install_log_assertion_steps
140
+ end
141
+
142
+ def install_memory_assertion_step
143
+ Then('the process {string} should consume less than {string} of memory') do |name, mem|
144
+ process = Nonnative.pool.process_by_name(name)
145
+ _, size, type = mem.split(/(\d+)/)
146
+ actual = process.memory.send(type)
147
+ size = size.to_i
148
+
149
+ expect(actual).to be < size
150
+ end
151
+ end
152
+
153
+ def install_error_assertion_steps
154
+ Then('starting the system should raise an error') do
155
+ expect(@start_error).to be_a(Nonnative::StartError)
156
+ end
157
+
158
+ Then('stopping the system should raise an error') do
159
+ expect(@stop_error).to be_a(Nonnative::StopError)
160
+ end
161
+ end
162
+
163
+ def install_log_assertion_steps
164
+ Then('I should see a log entry of {string} for process {string}') do |message, process|
165
+ process = Nonnative.configuration.process_by_name(process)
166
+ expect(Nonnative.log_lines(process.log, ->(l) { l.include?(message) }).first).to include(message)
167
+ end
168
+
169
+ Then('I should see a log entry of {string} in the file {string}') do |message, path|
170
+ expect(Nonnative.log_lines(path, ->(l) { l.include?(message) }).first).to include(message)
171
+ end
172
+ end
173
+ end
174
+
175
+ module Registration
176
+ extend ::Cucumber::Glue::Dsl
177
+ extend WorldHooks
178
+ extend ProxySteps
179
+ extend LifecycleSteps
180
+ extend Assertions
181
+
182
+ class << self
183
+ def install!
184
+ install_world
185
+ install_hooks
186
+ install_proxy_steps
187
+ install_state_steps
188
+ install_assertion_steps
189
+ end
190
+ end
191
+ end
192
+
193
+ class << self
194
+ def bootstrap!
195
+ return if @bootstrapped
196
+
197
+ dsl_singleton = ::Cucumber::Glue::Dsl.singleton_class
198
+ dsl_singleton.prepend(LanguageHook) unless dsl_singleton.ancestors.include?(LanguageHook)
199
+
200
+ @bootstrapped = true
201
+ install!
202
+ end
203
+
204
+ def install!
205
+ return if @installed
206
+ return unless ready?
207
+
208
+ Registration.install!
209
+ @installed = true
210
+ end
211
+
212
+ private
213
+
214
+ def ready?
215
+ !::Cucumber::Glue::Dsl.instance_variable_get(:@rb_language).nil?
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ Nonnative::Cucumber.bootstrap!
@@ -30,10 +30,11 @@ module Nonnative
30
30
  # @param request [Sinatra::Request] the incoming request
31
31
  # @return [Hash{String=>String}] headers to forward to the upstream
32
32
  def retrieve_headers(request)
33
- headers = request.env.map do |header, value|
34
- [header[5..].split('_').map(&:capitalize).join('-'), value] if header.start_with?('HTTP_')
33
+ headers = request.env.each_with_object({}) do |(header, value), result|
34
+ next unless forward_header?(header)
35
+
36
+ result[normalized_header_name(header)] = value
35
37
  end
36
- headers = headers.compact.to_h
37
38
 
38
39
  headers.except('Host', 'Accept-Encoding', 'Version')
39
40
  end
@@ -53,19 +54,45 @@ module Nonnative
53
54
  # @param uri [String] upstream URI
54
55
  # @param opts [Hash] RestClient options (e.g. headers)
55
56
  # @return [RestClient::Response] response for error statuses, otherwise RestClient return value
56
- def api_response(verb, uri, opts)
57
- client = RestClient::Resource.new(uri, opts)
57
+ def api_response(method:, url:, headers:, payload: nil)
58
+ options = { method:, url:, headers: }
59
+ options[:payload] = payload unless payload.nil?
58
60
 
59
- client.send(verb)
61
+ RestClient::Request.execute(options)
60
62
  rescue RestClient::Exception => e
61
63
  e.response
62
64
  end
63
65
 
66
+ # Extracts the request payload for verbs that can carry a body.
67
+ #
68
+ # @param request [Sinatra::Request] the incoming request
69
+ # @param verb [String] HTTP verb name (e.g. `"post"`)
70
+ # @return [String, nil] request payload for body-carrying verbs
71
+ def retrieve_payload(request, verb)
72
+ return unless %w[post put patch delete].include?(verb)
73
+
74
+ payload = request.body.read
75
+ payload unless payload.empty?
76
+ end
77
+
78
+ private
79
+
80
+ def forward_header?(header)
81
+ header.start_with?('HTTP_') || %w[CONTENT_TYPE CONTENT_LENGTH].include?(header)
82
+ end
83
+
84
+ def normalized_header_name(header)
85
+ header.delete_prefix('HTTP_').split('_').map(&:capitalize).join('-')
86
+ end
87
+
64
88
  %w[get post put patch delete].each do |verb|
65
89
  send(verb, /.*/) do
66
- uri = build_url(request, settings)
67
- opts = { headers: retrieve_headers(request) }
68
- res = api_response(verb, uri, opts)
90
+ res = api_response(
91
+ method: verb.to_sym,
92
+ url: build_url(request, settings),
93
+ headers: retrieve_headers(request),
94
+ payload: retrieve_payload(request, verb)
95
+ )
69
96
 
70
97
  status res.code
71
98
  res.body
@@ -3,8 +3,8 @@
3
3
  module Nonnative
4
4
  # Socket-pair variant used by the fault-injection proxy to simulate corrupted/incoherent traffic.
5
5
  #
6
- # When active, data written to the upstream socket is corrupted by shuffling the characters in the
7
- # payload before forwarding.
6
+ # When active, data written to the upstream socket is corrupted by shuffling the payload bytes
7
+ # before forwarding.
8
8
  #
9
9
  # This behavior is enabled by calling {Nonnative::FaultInjectionProxy#invalid_data}.
10
10
  #
@@ -12,7 +12,10 @@ module Nonnative
12
12
  # @see Nonnative::SocketPairFactory
13
13
  # @see Nonnative::SocketPair
14
14
  class InvalidDataSocketPair < SocketPair
15
- # Writes corrupted data to the socket by shuffling characters.
15
+ # Writes corrupted data to the socket by shuffling bytes.
16
+ #
17
+ # The payload must always change, otherwise short or repetitive inputs such as "test" can
18
+ # occasionally pass through unchanged and make fault-injection scenarios flaky.
16
19
  #
17
20
  # @param socket [IO] the socket to write to
18
21
  # @param data [String] the original payload
@@ -20,9 +23,18 @@ module Nonnative
20
23
  def write(socket, data)
21
24
  Nonnative.logger.info "shuffling socket data '#{socket.inspect}' for 'invalid_data' pair"
22
25
 
23
- data = data.chars.shuffle.join
26
+ super(socket, corrupt(data))
27
+ end
28
+
29
+ private
30
+
31
+ def corrupt(data)
32
+ bytes = data.bytes
33
+ corrupted = bytes.shuffle
34
+ return corrupted.pack('C*') unless corrupted == bytes
24
35
 
25
- super
36
+ corrupted[0] = (corrupted[0] + 1) % 256
37
+ corrupted.pack('C*')
26
38
  end
27
39
  end
28
40
  end