nonnative 3.30.0 → 3.32.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/README.md +9 -5
- data/lib/nonnative/cucumber.rb +2 -0
- data/lib/nonnative/fault_injection_proxy.rb +36 -0
- data/lib/nonnative/flaky_socket_pair.rb +45 -0
- data/lib/nonnative/http_proxy_server.rb +14 -7
- data/lib/nonnative/slicer_socket_pair.rb +69 -0
- data/lib/nonnative/socket_pair_factory.rb +8 -2
- data/lib/nonnative/version.rb +1 -1
- data/lib/nonnative.rb +5 -2
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 108231da69ec8bdf5cd71d3b0a2bb1e724c7b7826a3c33b631044873d2de71cb
|
|
4
|
+
data.tar.gz: 43b4fb6617eebce09db0b460aa0ad8117290a5a8f952eccbe33f8fbe89ad6766
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c9446f51fe18cd021c0baa25d0de2ab3389c01e3d1ec571decc3855c711737012cc5268f42acac6b604f9b9aa7bb957ce5197caf74d8150bf0e395291de78e3
|
|
7
|
+
data.tar.gz: 1b4ce6b1bb74f9952f93f66d8064ae90f66108e7fb2038f35e4d52e4dcfe02b40c58469402883c2bd737246d35eb4a44a132b8033a2aa06980e2d7baff580a29
|
data/README.md
CHANGED
|
@@ -276,8 +276,8 @@ The shipped steps are compatibility surface for downstream suites:
|
|
|
276
276
|
text: `Then I should see a log entry of {string} for process {string}` and
|
|
277
277
|
`Then I should see a log entry of {string} in the file {string}`.
|
|
278
278
|
- `Given I set the proxy for service {string} to {string}` accepts `close_all`, `reset_peer`, `delay`,
|
|
279
|
-
`timeout`, `invalid_data`, `bandwidth`, `limit_data`, or `reset`; the reset action
|
|
280
|
-
`Then I should reset the proxy for service {string}`.
|
|
279
|
+
`timeout`, `invalid_data`, `bandwidth`, `limit_data`, `slicer`, `flaky`, or `reset`; the reset action
|
|
280
|
+
step is `Then I should reset the proxy for service {string}`.
|
|
281
281
|
|
|
282
282
|
```cucumber
|
|
283
283
|
@manual
|
|
@@ -597,9 +597,11 @@ end
|
|
|
597
597
|
|
|
598
598
|
The system allows you to define an in-process HTTP forward proxy server for external systems, e.g. `api.github.com`. This is a server implementation, not a fault-injection service proxy.
|
|
599
599
|
|
|
600
|
-
The upstream scheme
|
|
601
|
-
`
|
|
602
|
-
`
|
|
600
|
+
The upstream scheme defaults to HTTPS on the scheme's default port; pass `scheme:`/`port:` to
|
|
601
|
+
`Nonnative::HTTPProxyServer.new` to target an `http://` upstream or a non-default port. The proxy
|
|
602
|
+
forwards the request path and query for `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, and
|
|
603
|
+
`OPTIONS`, while removing proxy credentials plus `Host` and `Accept-Encoding` before the upstream
|
|
604
|
+
request.
|
|
603
605
|
|
|
604
606
|
The proxy preserves the upstream status, body, and safe end-to-end response headers such as `Content-Type`, `ETag`, and application-specific metadata. It removes hop-by-hop, connection-nominated, proxy-authentication, and framing headers; `Set-Cookie`, `Location`, and `Content-Encoding` are not forwarded.
|
|
605
607
|
|
|
@@ -932,6 +934,8 @@ Clients connect to the service `host`/`port`, while the proxy forwards traffic t
|
|
|
932
934
|
- `invalid_data` - Forwards client requests unchanged, then corrupts upstream responses before they reach the client.
|
|
933
935
|
- `bandwidth` - Throttles forwarded throughput to `options.rate` kilobytes per second (1 KB = 1024 bytes) by sleeping in proportion to the bytes read, in both directions, so clients see a slow-but-alive dependency. When `rate` is absent or not positive, traffic forwards at full speed.
|
|
934
936
|
- `limit_data` - Forwards client requests unchanged, then sends the first `options.bytes` bytes of the upstream byte stream on each connection and gracefully closes the connection. When `bytes` is absent or not positive, traffic forwards at full speed.
|
|
937
|
+
- `slicer` - Forwards client requests unchanged, then writes each response to the client in `options.slice_size`-byte pieces, optionally separated by `options.slice_delay` seconds, so a client must perform multiple reads to reassemble the message. When `slice_size` is absent or not positive, traffic forwards at full speed.
|
|
938
|
+
- `flaky` - Fails a fraction of new connections (closed immediately, like `close_all`) controlled by `options.probability` (0.0-1.0), forwarding the rest normally, so a client's retry/reconnect logic sees both failures and successes while this state stays active. When `probability` is absent or not positive, traffic forwards at full speed.
|
|
935
939
|
|
|
936
940
|
###### 🧩 Fault Injection Services
|
|
937
941
|
|
data/lib/nonnative/cucumber.rb
CHANGED
|
@@ -16,6 +16,8 @@ module Nonnative
|
|
|
16
16
|
# - {#invalid_data}: forward requests unchanged and mutate upstream responses before they reach clients
|
|
17
17
|
# - {#bandwidth}: throttle forwarded throughput to a configured rate (KB/s)
|
|
18
18
|
# - {#limit_data}: forward a configured number of response bytes, then gracefully close
|
|
19
|
+
# - {#slicer}: forward responses to the client in small writes to force multi-`recv` reassembly
|
|
20
|
+
# - {#flaky}: fail a configurable fraction of new connections, forwarding the rest normally
|
|
19
21
|
# - {#reset}: return to healthy pass-through behavior
|
|
20
22
|
#
|
|
21
23
|
# State changes terminate any active connections so new connections observe the new behavior.
|
|
@@ -41,6 +43,11 @@ module Nonnative
|
|
|
41
43
|
# values forward at full speed
|
|
42
44
|
# - `bytes`: positive response byte limit used by {#limit_data}; absent or non-positive values
|
|
43
45
|
# use pass-through behavior
|
|
46
|
+
# - `slice_size`: positive response slice size (bytes) used by {#slicer}; absent or non-positive
|
|
47
|
+
# values use pass-through behavior
|
|
48
|
+
# - `slice_delay`: optional delay (seconds) between slices used by {#slicer}
|
|
49
|
+
# - `probability`: connection failure fraction (0.0-1.0) used by {#flaky}; absent or non-positive
|
|
50
|
+
# values use pass-through behavior
|
|
44
51
|
#
|
|
45
52
|
# @see Nonnative::Proxy
|
|
46
53
|
# @see Nonnative::SocketPairFactory
|
|
@@ -91,6 +98,10 @@ module Nonnative
|
|
|
91
98
|
server&.close
|
|
92
99
|
|
|
93
100
|
listener_thread = thread
|
|
101
|
+
# Closing the server is meant to wake the blocked `accept`, but a concurrently blocked
|
|
102
|
+
# `accept` is not reliably interrupted by `close` on every platform. Kill the listener so
|
|
103
|
+
# `stop` cannot hang joining an accept loop that never woke (a no-op once it has exited).
|
|
104
|
+
listener_thread&.kill
|
|
94
105
|
listener_thread&.join
|
|
95
106
|
|
|
96
107
|
@tcp_server = nil
|
|
@@ -166,6 +177,31 @@ module Nonnative
|
|
|
166
177
|
apply_state :limit_data
|
|
167
178
|
end
|
|
168
179
|
|
|
180
|
+
# Fragments upstream responses into small writes before forwarding to the client.
|
|
181
|
+
#
|
|
182
|
+
# Client requests are forwarded unchanged. Each response is split into
|
|
183
|
+
# `service.proxy.options[:slice_size]`-byte writes, optionally separated by
|
|
184
|
+
# `service.proxy.options[:slice_delay]` seconds, so a client's `recv` returns a strict prefix of
|
|
185
|
+
# the response rather than the whole message. When `slice_size` is absent or not positive, the
|
|
186
|
+
# connection forwards at full speed without slicing.
|
|
187
|
+
#
|
|
188
|
+
# @return [void]
|
|
189
|
+
def slicer
|
|
190
|
+
apply_state :slicer
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Fails a configurable fraction of new connections while forwarding the rest normally.
|
|
194
|
+
#
|
|
195
|
+
# The fraction is controlled by `service.proxy.options[:probability]` (0.0-1.0); absent or
|
|
196
|
+
# non-positive values behave like pass-through, and `1.0` fails every connection. Because each
|
|
197
|
+
# connection decides independently, clients that retry/reconnect can observe both failures and
|
|
198
|
+
# successes while this state stays active.
|
|
199
|
+
#
|
|
200
|
+
# @return [void]
|
|
201
|
+
def flaky
|
|
202
|
+
apply_state :flaky
|
|
203
|
+
end
|
|
204
|
+
|
|
169
205
|
# Resets the proxy back to healthy pass-through behavior.
|
|
170
206
|
#
|
|
171
207
|
# @return [void]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nonnative
|
|
4
|
+
# Socket-pair variant used by the fault-injection proxy to simulate a flapping dependency.
|
|
5
|
+
#
|
|
6
|
+
# When active, each new connection independently fails with probability
|
|
7
|
+
# `proxy.options[:probability]` (closed immediately, like {Nonnative::CloseAllSocketPair}) and
|
|
8
|
+
# otherwise forwards normally. Because the decision is made per connection rather than per state, a
|
|
9
|
+
# client that retries/reconnects can observe some attempts fail and others succeed while this fault
|
|
10
|
+
# state stays active, exercising retry/reconnect/circuit-breaker recovery rather than a fully down
|
|
11
|
+
# dependency. A missing or non-positive `probability` behaves like pass-through; `probability >= 1.0`
|
|
12
|
+
# fails every connection.
|
|
13
|
+
#
|
|
14
|
+
# This behavior is enabled by calling {Nonnative::FaultInjectionProxy#flaky}.
|
|
15
|
+
#
|
|
16
|
+
# @see Nonnative::FaultInjectionProxy
|
|
17
|
+
# @see Nonnative::SocketPairFactory
|
|
18
|
+
# @see Nonnative::SocketPair
|
|
19
|
+
# @see Nonnative::CloseAllSocketPair
|
|
20
|
+
class FlakySocketPair < SocketPair
|
|
21
|
+
# Fails the connection with the configured probability, otherwise forwards it normally.
|
|
22
|
+
#
|
|
23
|
+
# @param local_socket [TCPSocket] the accepted client socket
|
|
24
|
+
# @return [void]
|
|
25
|
+
def connect(local_socket)
|
|
26
|
+
return super unless fail?
|
|
27
|
+
|
|
28
|
+
Nonnative.logger.info "closing socket '#{local_socket.inspect}' for 'flaky' pair"
|
|
29
|
+
|
|
30
|
+
local_socket.close
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Decides whether this connection should fail, based on `proxy.options[:probability]`.
|
|
36
|
+
#
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def fail?
|
|
39
|
+
probability = proxy.options[:probability]
|
|
40
|
+
return false if probability.nil? || probability <= 0
|
|
41
|
+
|
|
42
|
+
rand < probability
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# Sinatra-based HTTP forward proxy server used as an in-process Nonnative server.
|
|
4
4
|
#
|
|
5
|
-
# The proxy receives inbound HTTP requests and forwards them to an upstream host
|
|
6
|
-
# the
|
|
5
|
+
# The proxy receives inbound HTTP requests and forwards them to an upstream host, defaulting to HTTPS
|
|
6
|
+
# on the scheme's default port but configurable to HTTP and/or a non-default port. It returns the
|
|
7
|
+
# upstream response status, body, and safe end-to-end response headers. Hop-by-hop,
|
|
7
8
|
# connection-nominated, proxy-authentication, framing, and deferred response headers are not forwarded.
|
|
8
9
|
#
|
|
9
10
|
# This file defines two classes:
|
|
@@ -69,10 +70,12 @@ module Nonnative
|
|
|
69
70
|
# Builds the upstream URL for the given request.
|
|
70
71
|
#
|
|
71
72
|
# @param request [Sinatra::Request] the incoming request
|
|
72
|
-
# @param settings [Sinatra::Base] Sinatra settings, expected to include `host`
|
|
73
|
-
# @return [String]
|
|
73
|
+
# @param settings [Sinatra::Base] Sinatra settings, expected to include `host`, `scheme`, and `port`
|
|
74
|
+
# @return [String] upstream URL
|
|
74
75
|
def build_url(request, settings)
|
|
75
|
-
|
|
76
|
+
uri_class = settings.scheme == 'http' ? URI::HTTP : URI::HTTPS
|
|
77
|
+
|
|
78
|
+
uri_class.build(host: settings.host, port: settings.port, path: request.path_info, query: request.query_string).to_s
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
# Executes the upstream request and returns the response.
|
|
@@ -193,11 +196,15 @@ module Nonnative
|
|
|
193
196
|
#
|
|
194
197
|
# @see Nonnative::HTTPServer
|
|
195
198
|
class HTTPProxyServer < Nonnative::HTTPServer
|
|
196
|
-
# @param host [String] upstream host to proxy to
|
|
199
|
+
# @param host [String] upstream host to proxy to
|
|
197
200
|
# @param service [Nonnative::ConfigurationServer] server configuration
|
|
198
|
-
|
|
201
|
+
# @param scheme [String] upstream scheme, `"http"` or `"https"`
|
|
202
|
+
# @param port [Integer, nil] upstream port; `nil` uses the scheme's default port
|
|
203
|
+
def initialize(host, service, scheme: 'https', port: nil)
|
|
199
204
|
http_service = Class.new(Nonnative::HTTPProxy) do
|
|
200
205
|
set :host, host
|
|
206
|
+
set :scheme, scheme
|
|
207
|
+
set :port, port
|
|
201
208
|
end
|
|
202
209
|
|
|
203
210
|
super(http_service.new, service)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nonnative
|
|
4
|
+
# Socket-pair variant used by the fault-injection proxy to fragment upstream responses.
|
|
5
|
+
#
|
|
6
|
+
# When active, client requests pass through unchanged, while each response written back to the
|
|
7
|
+
# client is split into `proxy.options[:slice_size]`-byte writes, optionally separated by
|
|
8
|
+
# `proxy.options[:slice_delay]` seconds, so a client that assumes one `recv` yields a whole protocol
|
|
9
|
+
# frame is forced to perform multiple reads and reassemble the message. A missing or non-positive
|
|
10
|
+
# `slice_size` leaves the connection in pass-through mode.
|
|
11
|
+
#
|
|
12
|
+
# This behavior is enabled by calling {Nonnative::FaultInjectionProxy#slicer}.
|
|
13
|
+
#
|
|
14
|
+
# @see Nonnative::FaultInjectionProxy
|
|
15
|
+
# @see Nonnative::SocketPairFactory
|
|
16
|
+
# @see Nonnative::SocketPair
|
|
17
|
+
class SlicerSocketPair < SocketPair
|
|
18
|
+
# Tracks the client socket so we can fragment only the response path.
|
|
19
|
+
#
|
|
20
|
+
# @param local_socket [TCPSocket] the accepted client socket
|
|
21
|
+
# @return [void]
|
|
22
|
+
def connect(local_socket)
|
|
23
|
+
@local_socket = local_socket
|
|
24
|
+
|
|
25
|
+
super
|
|
26
|
+
ensure
|
|
27
|
+
@local_socket = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Writes response data to the client in configured slices, leaving requests unchanged.
|
|
31
|
+
#
|
|
32
|
+
# @param socket [IO] the socket to write to
|
|
33
|
+
# @param data [String] the original payload
|
|
34
|
+
# @return [Integer] number of bytes written
|
|
35
|
+
def write(socket, data)
|
|
36
|
+
return super unless socket.equal?(@local_socket)
|
|
37
|
+
|
|
38
|
+
size = proxy.options[:slice_size]
|
|
39
|
+
return super if size.nil? || size <= 0
|
|
40
|
+
|
|
41
|
+
sliced_write(socket, data, size)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Writes `data` to `socket` in `size`-byte slices, sleeping `proxy.options[:slice_delay]` seconds
|
|
47
|
+
# between slices (when positive) to help defeat TCP write coalescing.
|
|
48
|
+
#
|
|
49
|
+
# @param socket [IO] the socket to write to
|
|
50
|
+
# @param data [String] the original payload
|
|
51
|
+
# @param size [Integer] the slice size in bytes
|
|
52
|
+
# @return [Integer] number of bytes written
|
|
53
|
+
def sliced_write(socket, data, size)
|
|
54
|
+
delay = proxy.options[:slice_delay]
|
|
55
|
+
offset = 0
|
|
56
|
+
written = 0
|
|
57
|
+
|
|
58
|
+
while offset < data.bytesize
|
|
59
|
+
chunk = data.byteslice(offset, size)
|
|
60
|
+
written += socket.write(chunk)
|
|
61
|
+
offset += chunk.bytesize
|
|
62
|
+
|
|
63
|
+
sleep(delay) if delay&.positive? && offset < data.bytesize
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
written
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -15,6 +15,8 @@ module Nonnative
|
|
|
15
15
|
# - `:invalid_data` -> {Nonnative::InvalidDataSocketPair}
|
|
16
16
|
# - `:bandwidth` -> {Nonnative::BandwidthSocketPair}
|
|
17
17
|
# - `:limit_data` -> {Nonnative::LimitDataSocketPair}
|
|
18
|
+
# - `:slicer` -> {Nonnative::SlicerSocketPair}
|
|
19
|
+
# - `:flaky` -> {Nonnative::FlakySocketPair}
|
|
18
20
|
#
|
|
19
21
|
# @see Nonnative::FaultInjectionProxy
|
|
20
22
|
# @see Nonnative::SocketPair
|
|
@@ -25,6 +27,8 @@ module Nonnative
|
|
|
25
27
|
# @see Nonnative::InvalidDataSocketPair
|
|
26
28
|
# @see Nonnative::BandwidthSocketPair
|
|
27
29
|
# @see Nonnative::LimitDataSocketPair
|
|
30
|
+
# @see Nonnative::SlicerSocketPair
|
|
31
|
+
# @see Nonnative::FlakySocketPair
|
|
28
32
|
class SocketPairFactory
|
|
29
33
|
PAIR_BY_STATE = {
|
|
30
34
|
close_all: CloseAllSocketPair,
|
|
@@ -33,7 +37,9 @@ module Nonnative
|
|
|
33
37
|
timeout: TimeoutSocketPair,
|
|
34
38
|
invalid_data: InvalidDataSocketPair,
|
|
35
39
|
bandwidth: BandwidthSocketPair,
|
|
36
|
-
limit_data: LimitDataSocketPair
|
|
40
|
+
limit_data: LimitDataSocketPair,
|
|
41
|
+
slicer: SlicerSocketPair,
|
|
42
|
+
flaky: FlakySocketPair
|
|
37
43
|
}.freeze
|
|
38
44
|
private_constant :PAIR_BY_STATE
|
|
39
45
|
|
|
@@ -41,7 +47,7 @@ module Nonnative
|
|
|
41
47
|
# Creates a socket-pair instance for the given proxy state.
|
|
42
48
|
#
|
|
43
49
|
# @param kind [Symbol] proxy state (e.g. `:none`, `:close_all`, `:reset_peer`, `:delay`, `:timeout`,
|
|
44
|
-
# `:invalid_data`, `:bandwidth`, `:limit_data`)
|
|
50
|
+
# `:invalid_data`, `:bandwidth`, `:limit_data`, `:slicer`, `:flaky`)
|
|
45
51
|
# @param proxy [Nonnative::ConfigurationProxy] proxy configuration (host/port/options)
|
|
46
52
|
# @return [Nonnative::SocketPair] a socket-pair implementation instance
|
|
47
53
|
def create(kind, proxy)
|
data/lib/nonnative/version.rb
CHANGED
data/lib/nonnative.rb
CHANGED
|
@@ -117,6 +117,8 @@ require 'nonnative/timeout_socket_pair'
|
|
|
117
117
|
require 'nonnative/invalid_data_socket_pair'
|
|
118
118
|
require 'nonnative/bandwidth_socket_pair'
|
|
119
119
|
require 'nonnative/limit_data_socket_pair'
|
|
120
|
+
require 'nonnative/slicer_socket_pair'
|
|
121
|
+
require 'nonnative/flaky_socket_pair'
|
|
120
122
|
require 'nonnative/socket_pair_factory'
|
|
121
123
|
require 'nonnative/go_executable'
|
|
122
124
|
require 'nonnative/cucumber'
|
|
@@ -349,10 +351,11 @@ module Nonnative
|
|
|
349
351
|
|
|
350
352
|
# Resets proxies for all currently started runners.
|
|
351
353
|
#
|
|
354
|
+
# No-op when called before {Nonnative.start}, since no pool exists yet.
|
|
355
|
+
#
|
|
352
356
|
# @return [void]
|
|
353
|
-
# @raise [NoMethodError] if called before {Nonnative.start} (because {Nonnative.pool} is nil)
|
|
354
357
|
def reset
|
|
355
|
-
Nonnative.pool
|
|
358
|
+
Nonnative.pool&.reset
|
|
356
359
|
end
|
|
357
360
|
|
|
358
361
|
private
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nonnative
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.32.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alejandro Falkowski
|
|
@@ -410,6 +410,7 @@ files:
|
|
|
410
410
|
- lib/nonnative/ed25519_key.rb
|
|
411
411
|
- lib/nonnative/error.rb
|
|
412
412
|
- lib/nonnative/fault_injection_proxy.rb
|
|
413
|
+
- lib/nonnative/flaky_socket_pair.rb
|
|
413
414
|
- lib/nonnative/go_executable.rb
|
|
414
415
|
- lib/nonnative/grpc_health.rb
|
|
415
416
|
- lib/nonnative/grpc_probe.rb
|
|
@@ -437,6 +438,7 @@ files:
|
|
|
437
438
|
- lib/nonnative/runner.rb
|
|
438
439
|
- lib/nonnative/server.rb
|
|
439
440
|
- lib/nonnative/service.rb
|
|
441
|
+
- lib/nonnative/slicer_socket_pair.rb
|
|
440
442
|
- lib/nonnative/socket_pair.rb
|
|
441
443
|
- lib/nonnative/socket_pair_factory.rb
|
|
442
444
|
- lib/nonnative/ssh_token.rb
|
|
@@ -470,7 +472,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
470
472
|
- !ruby/object:Gem::Version
|
|
471
473
|
version: '0'
|
|
472
474
|
requirements: []
|
|
473
|
-
rubygems_version: 4.0.
|
|
475
|
+
rubygems_version: 4.0.16
|
|
474
476
|
specification_version: 4
|
|
475
477
|
summary: Ruby-first end-to-end harness for testing systems implemented in other languages
|
|
476
478
|
test_files: []
|