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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +7 -5
- data/AGENTS.md +97 -198
- data/Gemfile.lock +21 -20
- data/README.md +32 -12
- data/lib/nonnative/configuration.rb +11 -10
- data/lib/nonnative/configuration_proxy.rb +13 -1
- data/lib/nonnative/cucumber.rb +219 -103
- data/lib/nonnative/http_proxy_server.rb +36 -9
- data/lib/nonnative/invalid_data_socket_pair.rb +17 -5
- data/lib/nonnative/pool.rb +114 -23
- data/lib/nonnative/process.rb +2 -1
- data/lib/nonnative/version.rb +1 -1
- data/lib/nonnative.rb +59 -15
- metadata +1 -1
|
@@ -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
|
|
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
|
|
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
|
data/lib/nonnative/cucumber.rb
CHANGED
|
@@ -1,105 +1,221 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
Given('I
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
34
|
-
|
|
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(
|
|
57
|
-
|
|
57
|
+
def api_response(method:, url:, headers:, payload: nil)
|
|
58
|
+
options = { method:, url:, headers: }
|
|
59
|
+
options[:payload] = payload unless payload.nil?
|
|
58
60
|
|
|
59
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
7
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
corrupted[0] = (corrupted[0] + 1) % 256
|
|
37
|
+
corrupted.pack('C*')
|
|
26
38
|
end
|
|
27
39
|
end
|
|
28
40
|
end
|