nonnative 3.29.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d81874eb81ecd99758c560530adaf6dd701ac6136258ec36b04a074312e45e65
4
- data.tar.gz: 05e90f242c9cd70a7c544705d2db49c593e7615ff7edfc28e6864154036e758b
3
+ metadata.gz: 108231da69ec8bdf5cd71d3b0a2bb1e724c7b7826a3c33b631044873d2de71cb
4
+ data.tar.gz: 43b4fb6617eebce09db0b460aa0ad8117290a5a8f952eccbe33f8fbe89ad6766
5
5
  SHA512:
6
- metadata.gz: 59abcf2ef7f4c1cf0785ab43dbca7af059441f8a65379601adc1204c8860955b462fc7fa8dac8b1bbb93cd7a449296cac36ea18bee692b67baad567b83cea3ad
7
- data.tar.gz: ac4407b3205ca54a8e2496e9e2ce2594ee8bf8d7fff8f9c0ae5bce929fdf4b707732c54c8b45ccb038e79bb34601cf52823a7ee99ae6c7ae05a8bbdfb60cc05a
6
+ metadata.gz: 7c9446f51fe18cd021c0baa25d0de2ab3389c01e3d1ec571decc3855c711737012cc5268f42acac6b604f9b9aa7bb957ce5197caf74d8150bf0e395291de78e3
7
+ data.tar.gz: 1b4ce6b1bb74f9952f93f66d8064ae90f66108e7fb2038f35e4d52e4dcfe02b40c58469402883c2bd737246d35eb4a44a132b8033a2aa06980e2d7baff580a29
data/README.md CHANGED
@@ -57,7 +57,10 @@ YAML configuration is loaded as data only: ERB is not evaluated and arbitrary Ru
57
57
  deserialized.
58
58
 
59
59
  > [!CAUTION]
60
- > Treat YAML configuration as plain data. ERB is not evaluated and arbitrary Ruby object tags are rejected.
60
+ > Treat YAML configuration as plain data. ERB is not evaluated, `${VAR}` values are not expanded,
61
+ > and arbitrary Ruby object tags are rejected. Unknown structural keys may be ignored, although YAML
62
+ > syntax, object safety, and supported value shapes are still validated. Keep values that vary by
63
+ > environment in programmatic Ruby configuration, where Ruby's `ENV` is available.
61
64
 
62
65
  High-level configuration fields:
63
66
  - `version`: configuration version (example: `"1.0"`).
@@ -74,7 +77,8 @@ Common runner fields:
74
77
 
75
78
  Process/server fields:
76
79
  - `ports`: client-facing ports. These are also used for readiness/shutdown port checks.
77
- - `timeout`: max time (seconds) for readiness/shutdown port checks. Defaults to `1.0`.
80
+ - `timeout`: max time (seconds) for each readiness/shutdown check. For processes, the same value also
81
+ bounds optional HTTP/gRPC probes and graceful child exit after the stop signal. Defaults to `1.0`.
78
82
  - `wait`: small sleep (seconds) between lifecycle steps.
79
83
  - `log`: per-runner log file used by process output redirection or server implementations.
80
84
 
@@ -89,7 +93,7 @@ Service fields:
89
93
  - `readiness`: optional list of startup readiness checks. Supported kind is `tcp`, which requires
90
94
  explicit `host` and `port`.
91
95
 
92
- Nonnative readiness and shutdown checks are TCP port checks by default. Configure process/server ports that are dedicated to the test run; if another process is already listening on the same endpoint, results are undefined. Processes can also opt into HTTP and gRPC readiness checks that run after TCP readiness succeeds. Services do not get automatic TCP readiness/shutdown checks, but can opt into TCP startup readiness for externally managed dependencies. HTTP readiness paths must be path-only values, such as `/test/readyz`; absolute URLs and scheme-relative URLs are rejected.
96
+ Nonnative readiness and shutdown checks are TCP port checks by default. Configure process/server ports that are dedicated to the test run; if another process is already listening on the same endpoint, results are undefined. Processes can also opt into HTTP and gRPC readiness checks that run after TCP readiness succeeds. HTTP readiness sends a plain HTTP `GET` without configurable request headers and is ready only when the final response is 2xx. gRPC readiness uses the standard health `Check` over an insecure channel and is ready only for `SERVING`. Non-ready responses are retried until the process timeout elapses. Services do not get automatic TCP readiness/shutdown checks, but can opt into TCP startup readiness for externally managed dependencies. HTTP readiness paths must be path-only values, such as `/test/readyz`; absolute URLs and scheme-relative URLs are rejected.
93
97
 
94
98
  > [!WARNING]
95
99
  > TCP readiness and shutdown checks only prove that a TCP port opened or closed. HTTP and gRPC readiness are process-only. Service readiness is TCP-only and should target the dependency endpoint that must be reachable before managed servers/processes start.
@@ -108,12 +112,19 @@ Nonnative.start
108
112
  Nonnative.stop
109
113
  ```
110
114
 
111
- `Nonnative.start` starts services first, then servers and processes. `Nonnative.stop` stops processes and servers first, then services. If startup fails, Nonnative rolls back runners that already started and raises `Nonnative::StartError`; shutdown failures raise `Nonnative::StopError`.
115
+ `Nonnative.start` runs ordered tiers: service lifecycle calls and optional readiness checks complete,
116
+ then server lifecycle and readiness checks complete, then process lifecycle and readiness checks run.
117
+ A failed service readiness check prevents later tiers; other collected startup errors trigger rollback
118
+ after the attempted tiers finish. `Nonnative.stop` reverses the tiers: processes, servers, then
119
+ services. Model dependencies in that direction; a managed server can satisfy a process dependency,
120
+ but a server cannot wait on a managed process. Startup failures raise `Nonnative::StartError`, and
121
+ shutdown failures raise `Nonnative::StopError`.
112
122
 
113
- > [!NOTE]
114
- > `Nonnative.start` / `Nonnative.stop` manage one lifecycle for the current pool.
115
- > Call `Nonnative.clear` before reconfiguring Nonnative or starting a new lifecycle in the same Ruby process.
116
- > `Nonnative.clear` clears memoized configuration, logger, observability client, and pool.
123
+ > [!WARNING]
124
+ > `Nonnative.clear` forgets the current pool; it does not stop live processes, server threads, or
125
+ > proxies. To reuse the same Ruby process, call `Nonnative.stop`, then `Nonnative.clear`, configure
126
+ > the next system, and call `Nonnative.start`. `clear` also clears the memoized configuration,
127
+ > logger, and observability client.
117
128
 
118
129
  ### 🧩 Test framework setup
119
130
 
@@ -167,6 +178,10 @@ response = Nonnative.observability.health(
167
178
  expect(response.code).to eq(200)
168
179
  ```
169
180
 
181
+ HTTP error statuses are returned as response objects so callers can inspect `.code` and `.body`.
182
+ Request timeouts and broken connections raise their RestClient exceptions; observability requests are
183
+ not retried automatically.
184
+
170
185
  `Nonnative.grpc_health` is a helper for the standard gRPC health checking protocol:
171
186
 
172
187
  ```ruby
@@ -180,6 +195,10 @@ health = Nonnative.grpc_health(
180
195
  expect(health.serving?).to eq(true)
181
196
  ```
182
197
 
198
+ The helper always uses an insecure plaintext gRPC channel. Pass `service: ''` or `nil` to check the
199
+ overall server. `check` returns the full `HealthCheckResponse` and propagates gRPC failures, while
200
+ `serving?` returns `false` for any non-`SERVING` status or gRPC failure.
201
+
183
202
  ### 🔑 Tokens
184
203
 
185
204
  `Nonnative.token` builds a signer for authenticating requests against a service under test. You pass the signing parameters directly (parsed from your own configuration); it is not coupled to any service's config format. The generated string is ready for `Nonnative::Header.auth_bearer`.
@@ -241,6 +260,32 @@ The repo’s own Cucumber suite also uses taxonomy tags to classify coverage:
241
260
 
242
261
  Requiring `nonnative` is enough; the Cucumber hooks and step definitions are installed lazily once Cucumber’s Ruby DSL is ready.
243
262
 
263
+ #### 🥒 Public Cucumber steps
264
+
265
+ The shipped steps are compatibility surface for downstream suites:
266
+
267
+ - `When I start the system` starts immediately. For expected failures, pair
268
+ `When I attempt to start the system` with `Then starting the system should raise an error`, or use
269
+ the equivalent stop steps.
270
+ - `Then I should see {string} as healthy` expects the configured health endpoint to return `200` and
271
+ a body containing neither the supplied service name nor `service unavailable`; `unhealthy` expects
272
+ `503` and a body identifying the service or `service unavailable`.
273
+ - `Then the process {string} should consume less than {string} of memory` accepts values such as
274
+ `25mb` for a started process.
275
+ - The two log steps search either a configured process log or an explicit file path for the requested
276
+ text: `Then I should see a log entry of {string} for process {string}` and
277
+ `Then I should see a log entry of {string} in the file {string}`.
278
+ - `Given I set the proxy for service {string} to {string}` accepts `close_all`, `reset_peer`, `delay`,
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
+
282
+ ```cucumber
283
+ @manual
284
+ Scenario: startup is expected to fail
285
+ When I attempt to start the system
286
+ Then starting the system should raise an error
287
+ ```
288
+
244
289
  ### ⚙️ Processes
245
290
 
246
291
  A process is some sort of command that you would run locally.
@@ -249,6 +294,12 @@ Programmatic `p.command` values must be callables that return a shell string or
249
294
  > [!TIP]
250
295
  > Prefer argv arrays for new process commands. Use shell strings only when you intentionally need shell parsing, expansion, or redirection.
251
296
 
297
+ Managed processes inherit the Ruby parent's working directory and environment; loading YAML from a
298
+ different directory does not change the child working directory. Relative command, config, log, and
299
+ generated-output paths resolve from that inherited directory. Configured `environment` values are
300
+ stringified and override variables with the same names while preserving the rest of the parent
301
+ environment.
302
+
252
303
  Set it up programmatically:
253
304
 
254
305
  ```ruby
@@ -338,6 +389,10 @@ Nonnative.configure do |config|
338
389
  end
339
390
  ```
340
391
 
392
+ On stop, Nonnative sends the configured signal (`INT` by default) and waits up to `timeout` for the
393
+ child to exit. If it remains alive, Nonnative sends `KILL` and reports the stop as unsuccessful, so
394
+ `Nonnative.stop` raises `Nonnative::StopError` even if the configured shutdown ports close.
395
+
341
396
  With cucumber you can also verify how much memory is used by the process:
342
397
 
343
398
  ```cucumber
@@ -465,8 +520,9 @@ module Nonnative
465
520
  end
466
521
  ```
467
522
 
468
- To run multiple Rack services on one managed port, pass a non-empty mount map. Each key must start
469
- with `/`, and the mounted application receives the remaining `PATH_INFO`:
523
+ To run multiple Rack services on one managed port, pass a non-empty `Rack::URLMap` mount map. Keys
524
+ can be path prefixes beginning with `/` or host-qualified URLs; the mounted application receives the
525
+ remaining `PATH_INFO`:
470
526
 
471
527
  ```ruby
472
528
  module Nonnative
@@ -541,6 +597,12 @@ end
541
597
 
542
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.
543
599
 
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.
605
+
544
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.
545
607
 
546
608
  Define your server:
@@ -783,9 +845,28 @@ These proxies can simulate different situations. Available proxy kinds are:
783
845
  Custom proxy kinds can be registered through `Nonnative.proxies`:
784
846
 
785
847
  ```ruby
848
+ class CustomProxy < Nonnative::Proxy
849
+ # Inherit #initialize(service), or call super from a custom initializer.
850
+ def start; end
851
+ def stop; end
852
+ def reset; end
853
+ end
854
+
786
855
  Nonnative.proxies['custom'] = CustomProxy
856
+
857
+ Nonnative.configure do |config|
858
+ config.service do |s|
859
+ s.name = 'dependency'
860
+ s.host = '127.0.0.1'
861
+ s.port = 12_345
862
+ s.proxy.kind = 'custom'
863
+ end
864
+ end
787
865
  ```
788
866
 
867
+ Custom proxies must accept the service configuration and implement `start`, `stop`, and `reset`;
868
+ Nonnative invokes those methods during service lifecycle and pool reset.
869
+
789
870
  Only services support proxies. For `fault_injection`, keep the service `host`/`port` as the client-facing proxy endpoint and use nested `proxy.host`/`proxy.port` for the upstream target behind the proxy.
790
871
  When service readiness is configured for a proxied dependency, set the readiness `host`/`port` to the upstream dependency, not the client-facing proxy listener.
791
872
 
@@ -853,9 +934,15 @@ Clients connect to the service `host`/`port`, while the proxy forwards traffic t
853
934
  - `invalid_data` - Forwards client requests unchanged, then corrupts upstream responses before they reach the client.
854
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.
855
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.
856
939
 
857
940
  ###### 🧩 Fault Injection Services
858
941
 
942
+ > [!WARNING]
943
+ > Every proxy state change closes active client connections so that new connections observe the new
944
+ > state. Apply the state before connecting, and reconnect after changing or resetting it.
945
+
859
946
  Set the proxy state programmatically:
860
947
 
861
948
  ```ruby
@@ -902,6 +989,11 @@ YAML `go:` configuration is for Go test binaries compiled with `go test -c`. It
902
989
  > [!IMPORTANT]
903
990
  > If `tools` is omitted or empty, Nonnative enables all Go tools: `prof`, `trace`, and `cover`. Provide a subset, such as `tools: [cover]`, to limit the generated `-test.*` flags.
904
991
 
992
+ Create the configured `output` directory before starting Nonnative; the helper does not create it and
993
+ the Go test binary must be able to write there. Artifact names include the executable basename
994
+ without its extension, the command, and a random four-character suffix, for example
995
+ `reports/your_binary-sub_command-Ab12-cpu.prof`.
996
+
905
997
  To get this to work you will need to create a `main_test.go` file with these contents:
906
998
 
907
999
  ```go
@@ -60,14 +60,15 @@ module Nonnative
60
60
  # @return [Array<Nonnative::ConfigurationServer>] configured in-process servers
61
61
  attr_accessor :servers
62
62
 
63
- # @return [Array<Nonnative::ConfigurationService>] configured services (proxy-only)
63
+ # @return [Array<Nonnative::ConfigurationService>] configured external dependencies
64
64
  attr_accessor :services
65
65
 
66
66
  # Loads a configuration file and appends its runners to this instance.
67
67
  #
68
- # The file is loaded using safe YAML parsing. ERB is not evaluated,
69
- # arbitrary object deserialization is not allowed, top-level attributes are copied onto this object,
70
- # and runner sections are transformed into configuration runner objects.
68
+ # The file is loaded using safe YAML parsing. ERB and environment placeholders are not evaluated,
69
+ # arbitrary object deserialization is not allowed, and unknown structural keys may be ignored.
70
+ # Top-level supported attributes are copied onto this object, and runner sections are transformed
71
+ # into configuration runner objects.
71
72
  #
72
73
  # @param path [String] path to a configuration file (typically YAML)
73
74
  # @return [void]
@@ -108,8 +109,8 @@ module Nonnative
108
109
 
109
110
  # Adds a service configuration entry.
110
111
  #
111
- # A "service" does not manage a Ruby thread or OS process; it exists so that a proxy can be started
112
- # and controlled for an external dependency.
112
+ # A "service" represents an externally managed dependency. It does not manage a Ruby thread or OS
113
+ # process, but it can wait for TCP readiness and optionally control a proxy for the dependency.
113
114
  #
114
115
  # @yieldparam service [Nonnative::ConfigurationService]
115
116
  # @return [void]
@@ -11,6 +11,8 @@ module Nonnative
11
11
  # @see Nonnative::Configuration
12
12
  # @see Nonnative::Process
13
13
  class ConfigurationProcess < ConfigurationRunner
14
+ # The child inherits the parent Ruby process's working directory.
15
+ #
14
16
  # @return [Proc] a callable that returns the command to execute
15
17
  # as a shell string or argv array
16
18
  # (e.g. `-> { "./bin/api" }` or `-> { ["./bin/api", "--port", "8080"] }`)
@@ -19,12 +21,15 @@ module Nonnative
19
21
  # @return [String, nil] signal name to use for stopping (defaults to `"INT"` when not set)
20
22
  attr_accessor :signal
21
23
 
22
- # @return [Numeric] readiness timeout (seconds) used when waiting for ports to open/close (defaults to `1.0`)
24
+ # @return [Numeric] timeout (seconds) independently applied to child exit, port checks, and each
25
+ # optional HTTP/gRPC readiness probe (defaults to `1.0`)
23
26
  attr_accessor :timeout
24
27
 
25
28
  # @return [String] log file path to append process stdout/stderr to
26
29
  attr_accessor :log
27
30
 
31
+ # Values are stringified and override matching variables in the inherited parent environment.
32
+ #
28
33
  # @return [Hash, nil] environment variables to pass to the spawned process
29
34
  attr_accessor :environment
30
35
 
@@ -4,7 +4,9 @@ module Nonnative
4
4
  # Readiness check configuration for a managed process.
5
5
  #
6
6
  # Readiness is optional. When present, each check declares a `kind` and explicit endpoint details
7
- # to poll after TCP readiness succeeds.
7
+ # to poll after TCP readiness succeeds. HTTP checks issue a plain unauthenticated GET and accept a
8
+ # final 2xx response. gRPC checks use the standard health Check over an insecure channel and accept
9
+ # only SERVING. Non-ready results are retried until the process timeout elapses.
8
10
  class ConfigurationReadiness
9
11
  KINDS = %w[http grpc].freeze
10
12
 
@@ -12,7 +12,7 @@ module Nonnative
12
12
  # @see Nonnative::ConfigurationServer
13
13
  # @see Nonnative::ConfigurationService
14
14
  class ConfigurationRunner
15
- # Default bounded readiness/shutdown timeout for process and server runners.
15
+ # Default bounded lifecycle/readiness timeout for configured runners.
16
16
  DEFAULT_TIMEOUT = 1.0
17
17
 
18
18
  # @return [String, nil] runner name used for lookup (for example via `pool.process_by_name`)
@@ -3,15 +3,15 @@
3
3
  module Nonnative
4
4
  # Service-specific configuration.
5
5
  #
6
- # A "service" is proxy-only: it does not start a Ruby thread or OS process. It exists so Nonnative can
7
- # start and control a proxy in front of an external dependency.
6
+ # A "service" represents an externally managed dependency. It does not start a Ruby thread or OS
7
+ # process. It can wait for TCP readiness and can optionally run a proxy in front of the dependency.
8
8
  #
9
9
  # Instances are usually created through {Nonnative::Configuration#service}.
10
10
  #
11
11
  # @see Nonnative::Configuration
12
12
  # @see Nonnative::Service
13
13
  class ConfigurationService < ConfigurationRunner
14
- # @return [Integer] client-facing port used by the service proxy
14
+ # @return [Integer] client-facing dependency port, used as the listener when a proxy is enabled
15
15
  attr_accessor :port
16
16
 
17
17
  # @return [Numeric] readiness timeout (seconds) used when waiting for service readiness
@@ -61,6 +61,8 @@ module Nonnative
61
61
  'invalid_data' => :invalid_data,
62
62
  'bandwidth' => :bandwidth,
63
63
  'limit_data' => :limit_data,
64
+ 'slicer' => :slicer,
65
+ 'flaky' => :flaky,
64
66
  'reset' => :reset
65
67
  }.freeze
66
68
 
@@ -3,7 +3,8 @@
3
3
  module Nonnative
4
4
  # Parent error for all Nonnative errors.
5
5
  #
6
- # Catch this error type if you want to handle any exception raised by this gem.
6
+ # Rescue this type to handle Nonnative-specific errors. Public APIs may also raise standard Ruby,
7
+ # dependency, filesystem, or transport exceptions that are not subclasses of this class.
7
8
  #
8
9
  # @see Nonnative::StartError
9
10
  # @see Nonnative::StopError
@@ -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.
@@ -37,8 +39,15 @@ module Nonnative
37
39
  # - `delay`: delay duration in seconds used by {#delay}
38
40
  # - `jitter`: optional random offset (seconds) added in `-jitter..jitter` to each `delay` (a
39
41
  # negative value uses its magnitude), so clients see variable latency instead of a flat value
42
+ # - `rate`: positive throughput limit in KB/s used by {#bandwidth}; absent or non-positive
43
+ # values forward at full speed
40
44
  # - `bytes`: positive response byte limit used by {#limit_data}; absent or non-positive values
41
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
42
51
  #
43
52
  # @see Nonnative::Proxy
44
53
  # @see Nonnative::SocketPairFactory
@@ -89,6 +98,10 @@ module Nonnative
89
98
  server&.close
90
99
 
91
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
92
105
  listener_thread&.join
93
106
 
94
107
  @tcp_server = nil
@@ -164,6 +177,31 @@ module Nonnative
164
177
  apply_state :limit_data
165
178
  end
166
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
+
167
205
  # Resets the proxy back to healthy pass-through behavior.
168
206
  #
169
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
@@ -20,6 +20,7 @@ module Nonnative
20
20
  # If `tools` is `nil` or empty, all tools (`prof`, `trace`, `cover`) are enabled.
21
21
  #
22
22
  # Parameter strings are parsed into argv words using shell-style quoting.
23
+ # The output directory must already exist and be writable; this helper does not create it.
23
24
  #
24
25
  # @example
25
26
  # executable = Nonnative::GoExecutable.new(%w[prof cover], './svc.test', 'reports')
@@ -31,7 +32,7 @@ module Nonnative
31
32
  class GoExecutable
32
33
  # @param tools [Array<String>, nil] tool names to enable (see class docs)
33
34
  # @param exec [String] path to the compiled Go test binary
34
- # @param output [String] output directory for generated files
35
+ # @param output [String] existing writable output directory for generated files
35
36
  def initialize(tools, exec, output)
36
37
  @tools = tools.nil? || tools.empty? ? %w[prof trace cover] : tools
37
38
  @exec = exec
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
- # Client helper for the standard gRPC health checking protocol.
4
+ # Client helper for the standard gRPC health checking protocol over an insecure plaintext channel.
5
+ # An empty or nil service name checks overall server health.
5
6
  class GRPCHealth
6
7
  NETWORK_ERRORS = [
7
8
  GRPC::BadStatus,
@@ -10,7 +11,7 @@ module Nonnative
10
11
 
11
12
  # @param host [String] gRPC server host
12
13
  # @param port [Integer] gRPC server port
13
- # @param service [String] gRPC health service name
14
+ # @param service [String, nil] gRPC health service name, or empty/nil for overall server health
14
15
  # @param timeout [Numeric] default call timeout in seconds
15
16
  def initialize(host:, port:, service:, timeout: 1)
16
17
  @host = host
@@ -21,6 +22,8 @@ module Nonnative
21
22
 
22
23
  # Calls the gRPC health check endpoint.
23
24
  #
25
+ # gRPC status and network failures are propagated to the caller.
26
+ #
24
27
  # @param deadline [Time] gRPC deadline
25
28
  # @return [Grpc::Health::V1::HealthCheckResponse]
26
29
  def check(deadline: Time.now + timeout)
@@ -29,6 +32,8 @@ module Nonnative
29
32
 
30
33
  # Returns true when the gRPC health endpoint reports SERVING.
31
34
  #
35
+ # Returns `false` for non-SERVING responses and gRPC status/network failures.
36
+ #
32
37
  # @param deadline [Time] gRPC deadline
33
38
  # @return [Boolean]
34
39
  def serving?(deadline: Time.now + timeout)
@@ -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 over HTTPS, returning
6
- # the upstream response status, body, and safe end-to-end response headers. Hop-by-hop,
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] HTTPS URL for the upstream request
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
- URI::HTTPS.build(host: settings.host, path: request.path_info, query: request.query_string).to_s
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 (HTTPS)
199
+ # @param host [String] upstream host to proxy to
197
200
  # @param service [Nonnative::ConfigurationServer] server configuration
198
- def initialize(host, service)
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)
@@ -36,9 +36,10 @@ module Nonnative
36
36
  class HTTPServer < Nonnative::Server
37
37
  # Creates a Puma server for the given HTTP service and runner configuration.
38
38
  #
39
- # @param http_service [#call, Hash<String, #call>] an HTTP service instance or mount map
39
+ # @param http_service [#call, Hash<String, #call>] an HTTP service instance or `Rack::URLMap`
40
+ # mount map using path prefixes or host-qualified URLs
40
41
  # @param service [Nonnative::ConfigurationServer] server configuration
41
- # @raise [ArgumentError] if `http_service` is an empty mount map
42
+ # @raise [ArgumentError] if `http_service` is an empty mount map or contains an invalid mount path
42
43
  def initialize(http_service, service)
43
44
  if http_service.is_a?(Hash)
44
45
  raise ArgumentError, 'HTTP server requires at least one service mount' if http_service.empty?
@@ -13,7 +13,9 @@ module Nonnative
13
13
  # - `/<name>/metrics`
14
14
  #
15
15
  # Requests are performed using {Nonnative::HTTPClient}, so callers may pass RestClient options
16
- # such as `headers`, `open_timeout`, and `read_timeout`.
16
+ # such as `headers`, `open_timeout`, and `read_timeout`. HTTP error statuses are returned as response
17
+ # objects; timeouts and broken connections raise their RestClient exceptions. Requests are not
18
+ # retried automatically.
17
19
  #
18
20
  # @example
19
21
  # Nonnative.configure do |config|
@@ -6,10 +6,11 @@ module Nonnative
6
6
  # A pool is created when {Nonnative.start} is called and is accessible via {Nonnative.pool}.
7
7
  #
8
8
  # Lifecycle order is important:
9
- # - On start: services first, then servers/processes (in parallel port-check threads)
10
- # - On stop: processes/servers first, then services
9
+ # - On start: services, then servers, then processes; each tier completes readiness before the next
10
+ # - On stop: processes, then servers, then services
11
11
  #
12
- # Readiness and shutdown are determined via TCP port checks ({Nonnative::Ports#open?} / {Nonnative::Ports#closed?}).
12
+ # Readiness uses TCP port checks plus optional process HTTP/gRPC probes and optional service TCP
13
+ # checks. Shutdown uses configured TCP port checks.
13
14
  #
14
15
  # @see Nonnative.start
15
16
  # @see Nonnative.stop
@@ -25,9 +26,9 @@ module Nonnative
25
26
 
26
27
  # Starts all configured runners and collects lifecycle and readiness errors.
27
28
  #
28
- # Services are started first (proxy-only) and checked for opt-in readiness, then servers and processes are
29
- # started and checked for readiness. Each readiness failure is described with the runner and, for a process
30
- # that exited early, its termination detail.
29
+ # Externally managed services are handled first and checked for opt-in readiness. Servers are then
30
+ # started and checked for readiness, followed by processes. Each readiness failure is described
31
+ # with the runner and, for a process that exited early, its termination detail.
31
32
  #
32
33
  # @return [Array<String>] lifecycle and readiness-check errors collected while starting
33
34
  def start
@@ -45,7 +46,8 @@ module Nonnative
45
46
 
46
47
  # Stops all configured runners and collects lifecycle and shutdown errors.
47
48
  #
48
- # Processes and servers are stopped first and checked for shutdown, then services are stopped (proxy-only).
49
+ # Processes and servers are stopped first and checked for shutdown, then service proxies are
50
+ # stopped when configured.
49
51
  #
50
52
  # @return [Array<String>] lifecycle and shutdown-check errors collected while stopping
51
53
  def stop
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
- # Performs aggregate TCP readiness/shutdown checks for all configured runner ports.
4
+ # Performs aggregate readiness and shutdown checks for a configured runner.
5
5
  #
6
- # A runner is considered ready only when every configured port is open, and stopped only when every
7
- # configured port is closed.
6
+ # A runner is considered ready only when every configured port is open and every configured
7
+ # HTTP/gRPC readiness probe reports ready, and stopped only when every configured port is closed.
8
8
  #
9
9
  # @see Nonnative::Port
10
10
  class Ports
@@ -15,7 +15,8 @@ module Nonnative
15
15
  @readiness = readiness_probes
16
16
  end
17
17
 
18
- # Returns whether all configured ports become connectable before their timeouts elapse.
18
+ # Returns whether all configured ports become connectable and all configured HTTP/gRPC readiness
19
+ # probes report ready before their timeouts elapse.
19
20
  #
20
21
  # @return [Boolean]
21
22
  def open?
@@ -6,7 +6,8 @@ module Nonnative
6
6
  # A process runner:
7
7
  # - spawns a child process using the configured command and environment,
8
8
  # - waits briefly (via the runner `wait`), and
9
- # - participates in readiness/shutdown via TCP port checks orchestrated by {Nonnative::Pool}.
9
+ # - participates in TCP readiness/shutdown checks plus optional HTTP/gRPC readiness probes
10
+ # orchestrated by {Nonnative::Pool}.
10
11
  #
11
12
  # The underlying configuration is a {Nonnative::ConfigurationProcess}.
12
13
  #
@@ -46,6 +47,8 @@ module Nonnative
46
47
  # Stops the process if it is running.
47
48
  #
48
49
  # The process is signalled using the configured signal (defaults to `INT` when not set).
50
+ # If it does not exit before the configured timeout, it is killed and the returned success value
51
+ # is `false`; {Nonnative.stop} reports that outcome as a {Nonnative::StopError}.
49
52
  #
50
53
  # @return [Array<(Integer, Boolean)>]
51
54
  # a tuple of:
@@ -10,7 +10,8 @@ module Nonnative
10
10
  # implementation-specific; for example {Nonnative::FaultInjectionProxy#host} and
11
11
  # {Nonnative::FaultInjectionProxy#port} return the upstream target behind the proxy.
12
12
  #
13
- # Concrete proxies typically implement these public methods:
13
+ # Custom proxies must accept the service configuration in `initialize` and implement these public
14
+ # methods, which the service and pool invoke unconditionally:
14
15
  # - `start`: begin proxying (bind/listen, start threads, etc)
15
16
  # - `stop`: stop proxying and release resources
16
17
  # - `reset`: return proxy behavior to a healthy/default state
@@ -8,7 +8,7 @@ module Nonnative
8
8
  #
9
9
  # - {Nonnative::Process} for OS-level child processes
10
10
  # - {Nonnative::Server} for in-process Ruby servers (threads)
11
- # - {Nonnative::Service} for proxy-only external dependencies
11
+ # - {Nonnative::Service} for externally managed dependencies with optional readiness/proxying
12
12
  #
13
13
  # @see Nonnative::Process
14
14
  # @see Nonnative::Server
@@ -3,9 +3,9 @@
3
3
  module Nonnative
4
4
  # Runtime runner for an external dependency.
5
5
  #
6
- # A service runner does not manage an OS process or Ruby thread. It exists so Nonnative can manage
7
- # a proxy lifecycle (start/stop/reset) for an external service that is managed elsewhere (for example
8
- # a database running in Docker).
6
+ # A service runner does not manage an OS process or Ruby thread. It represents a dependency managed
7
+ # elsewhere (for example a database running in Docker), supports optional TCP readiness, and manages
8
+ # an optional proxy lifecycle (start/stop/reset).
9
9
  #
10
10
  # The underlying configuration is a {Nonnative::ConfigurationService}.
11
11
  #
@@ -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
@@ -4,7 +4,9 @@ module Nonnative
4
4
  # Base socket-pair implementation used by TCP proxies.
5
5
  #
6
6
  # A socket-pair connects an accepted local socket to a remote upstream socket and forwards bytes
7
- # in both directions until one side closes.
7
+ # in both directions. When one direction reaches EOF (for example a client that half-closes its
8
+ # write side after sending a request), the paired write side is closed and the other direction
9
+ # keeps forwarding until it too ends, so an in-flight response is delivered rather than dropped.
8
10
  #
9
11
  # This is used by {Nonnative::FaultInjectionProxy} to implement pass-through forwarding, and is
10
12
  # subclassed to inject failures (close immediately, delay reads, corrupt writes, etc).
@@ -31,12 +33,14 @@ module Nonnative
31
33
  def connect(local_socket)
32
34
  @local_socket = local_socket
33
35
  @remote_socket = create_remote_socket
36
+ @open = [@local_socket, @remote_socket]
34
37
 
35
38
  loop do
36
- ready = select([@local_socket, @remote_socket], nil, nil)
39
+ ready = select(@open, nil, nil)
37
40
 
38
41
  break if pipe?(ready, @local_socket, @remote_socket)
39
42
  break if pipe?(ready, @remote_socket, @local_socket)
43
+ break if @open.empty?
40
44
  end
41
45
  ensure
42
46
  Nonnative.logger.info "finished connect for local socket '#{@local_socket.inspect}' and '#{@remote_socket&.inspect}' for 'socket_pair'"
@@ -68,6 +72,10 @@ module Nonnative
68
72
 
69
73
  # Pipes data from `source_socket` to `destination_socket` when the source is readable.
70
74
  #
75
+ # On EOF the source is half-closed rather than terminating the whole connection: the destination
76
+ # write side is closed and the source is removed from the forwarding set, so the opposite
77
+ # direction keeps forwarding until it also ends.
78
+ #
71
79
  # @param ready [Array<Array<IO>>] the result from `select`
72
80
  # @param source_socket [IO] readable side
73
81
  # @param destination_socket [IO] writable side
@@ -75,7 +83,7 @@ module Nonnative
75
83
  def pipe?(ready, source_socket, destination_socket)
76
84
  if ready[0].include?(source_socket)
77
85
  data = read(source_socket)
78
- return true if data.empty?
86
+ return half_close(source_socket, destination_socket) if data.empty?
79
87
 
80
88
  write destination_socket, data
81
89
  end
@@ -104,6 +112,21 @@ module Nonnative
104
112
  socket.write(data)
105
113
  end
106
114
 
115
+ # Half-closes one direction after its source reaches EOF: closes the destination write side and
116
+ # removes the source from the forwarding set, so the opposite direction keeps forwarding.
117
+ #
118
+ # @param source_socket [IO] the side that reached EOF
119
+ # @param destination_socket [IO] the paired side whose write half is closed
120
+ # @return [false] so the forwarding loop continues until both directions have ended
121
+ def half_close(source_socket, destination_socket)
122
+ @open.delete(source_socket)
123
+ destination_socket.close_write
124
+
125
+ false
126
+ rescue IOError, SystemCallError
127
+ false
128
+ end
129
+
107
130
  def close_socket(socket)
108
131
  return if socket.nil? || socket.closed?
109
132
 
@@ -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)
@@ -4,5 +4,5 @@ module Nonnative
4
4
  # The current gem version.
5
5
  #
6
6
  # @return [String]
7
- VERSION = '3.29.0'
7
+ VERSION = '3.32.0'
8
8
  end
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'
@@ -188,7 +190,7 @@ module Nonnative
188
190
  # Use this when passing a command string directly to `spawn`.
189
191
  #
190
192
  # @param tools [Array<String>] enabled tool names (e.g. `["prof", "trace", "cover"]`)
191
- # @param output [String] directory where outputs should be written
193
+ # @param output [String] existing writable directory where outputs should be written
192
194
  # @param exec [String] the test binary (or wrapper) to execute
193
195
  # @param cmd [String] the command argument passed to the test binary
194
196
  # @param params [Array<String>] extra parameter strings for the command
@@ -202,7 +204,7 @@ module Nonnative
202
204
  # Use this when passing argv entries directly to `spawn`.
203
205
  #
204
206
  # @param tools [Array<String>] enabled tool names (e.g. `["prof", "trace", "cover"]`)
205
- # @param output [String] directory where outputs should be written
207
+ # @param output [String] existing writable directory where outputs should be written
206
208
  # @param exec [String] the test binary (or wrapper) to execute
207
209
  # @param cmd [String] the command argument passed to the test binary
208
210
  # @param params [Array<String>] extra parameter strings for the command
@@ -237,7 +239,7 @@ module Nonnative
237
239
  #
238
240
  # @param host [String] gRPC server host
239
241
  # @param port [Integer] gRPC server port
240
- # @param service [String] gRPC health service name
242
+ # @param service [String, nil] gRPC health service name, or empty/nil for overall server health
241
243
  # @param timeout [Numeric] default call timeout in seconds
242
244
  # @return [Nonnative::GRPCHealth]
243
245
  def grpc_health(host:, port:, service:, timeout: 1) = Nonnative::GRPCHealth.new(host: host, port: port, service: service, timeout: timeout)
@@ -265,7 +267,8 @@ module Nonnative
265
267
 
266
268
  # Starts all configured services, servers, and processes, and waits for readiness.
267
269
  #
268
- # Readiness is determined by attempting to connect to each runner's configured host/ports.
270
+ # Readiness is determined by port checks, plus optional process HTTP/gRPC readiness and optional
271
+ # service TCP readiness.
269
272
  #
270
273
  # @return [void]
271
274
  # @raise [Nonnative::StartError] if one or more runners fail to start or become ready in time
@@ -325,6 +328,8 @@ module Nonnative
325
328
 
326
329
  # Clears the memoized pool instance.
327
330
  #
331
+ # This does not stop runners owned by the pool. Call {Nonnative.stop} before clearing a live pool.
332
+ #
328
333
  # @return [void]
329
334
  def clear_pool
330
335
  @pool = nil
@@ -332,8 +337,9 @@ module Nonnative
332
337
 
333
338
  # Clears memoized configuration, logger, observability client, and pool.
334
339
  #
335
- # Call this before reconfiguring Nonnative or starting a new lifecycle in the same Ruby process.
336
- # `start`/`stop` are intended to manage one lifecycle for the current pool.
340
+ # This does not stop processes, server threads, or proxies. To reconfigure in the same Ruby
341
+ # process, call {Nonnative.stop}, then `clear`, configure the next system, and call
342
+ # {Nonnative.start}.
337
343
  #
338
344
  # @return [void]
339
345
  def clear
@@ -345,10 +351,11 @@ module Nonnative
345
351
 
346
352
  # Resets proxies for all currently started runners.
347
353
  #
354
+ # No-op when called before {Nonnative.start}, since no pool exists yet.
355
+ #
348
356
  # @return [void]
349
- # @raise [NoMethodError] if called before {Nonnative.start} (because {Nonnative.pool} is nil)
350
357
  def reset
351
- Nonnative.pool.reset
358
+ Nonnative.pool&.reset
352
359
  end
353
360
 
354
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.29.0
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.11
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: []