nonnative 1.107.0 → 1.108.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 +8 -6
- data/.rubocop.yml +3 -0
- data/AGENTS.md +248 -0
- data/CHANGELOG.md +104 -0
- data/Gemfile.lock +53 -51
- data/README.md +51 -32
- data/lib/nonnative/close_all_socket_pair.rb +14 -0
- data/lib/nonnative/configuration.rb +67 -0
- data/lib/nonnative/configuration_process.rb +14 -0
- data/lib/nonnative/configuration_proxy.rb +28 -0
- data/lib/nonnative/configuration_runner.rb +44 -0
- data/lib/nonnative/configuration_server.rb +12 -0
- data/lib/nonnative/configuration_service.rb +9 -0
- data/lib/nonnative/delay_socket_pair.rb +15 -0
- data/lib/nonnative/error.rb +7 -0
- data/lib/nonnative/fault_injection_proxy.rb +63 -0
- data/lib/nonnative/go_command.rb +34 -0
- data/lib/nonnative/grpc_server.rb +30 -0
- data/lib/nonnative/header.rb +44 -0
- data/lib/nonnative/http_client.rb +45 -0
- data/lib/nonnative/http_proxy_server.rb +62 -1
- data/lib/nonnative/http_server.rb +40 -0
- data/lib/nonnative/invalid_data_socket_pair.rb +15 -0
- data/lib/nonnative/no_proxy.rb +35 -0
- data/lib/nonnative/not_found_error.rb +7 -0
- data/lib/nonnative/observability.rb +44 -0
- data/lib/nonnative/pool.rb +50 -0
- data/lib/nonnative/port.rb +26 -0
- data/lib/nonnative/process.rb +29 -0
- data/lib/nonnative/proxy.rb +24 -0
- data/lib/nonnative/proxy_factory.rb +16 -0
- data/lib/nonnative/runner.rb +30 -0
- data/lib/nonnative/server.rb +28 -0
- data/lib/nonnative/service.rb +16 -0
- data/lib/nonnative/socket_pair.rb +46 -0
- data/lib/nonnative/socket_pair_factory.rb +21 -0
- data/lib/nonnative/start_error.rb +5 -0
- data/lib/nonnative/stop_error.rb +5 -0
- data/lib/nonnative/timeout.rb +20 -0
- data/lib/nonnative/version.rb +4 -1
- data/lib/nonnative.rb +128 -0
- metadata +3 -2
data/lib/nonnative/process.rb
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Runtime runner that manages an OS-level child process.
|
|
5
|
+
#
|
|
6
|
+
# A process runner:
|
|
7
|
+
# - starts the configured proxy (if any),
|
|
8
|
+
# - spawns a child process using the configured command and environment,
|
|
9
|
+
# - waits briefly (via the runner `wait`), and
|
|
10
|
+
# - participates in readiness/shutdown via TCP port checks orchestrated by {Nonnative::Pool}.
|
|
11
|
+
#
|
|
12
|
+
# The underlying configuration is a {Nonnative::ConfigurationProcess}.
|
|
13
|
+
#
|
|
14
|
+
# @see Nonnative::ConfigurationProcess
|
|
15
|
+
# @see Nonnative::Pool
|
|
4
16
|
class Process < Runner
|
|
17
|
+
# @param service [Nonnative::ConfigurationProcess] process configuration
|
|
5
18
|
def initialize(service)
|
|
6
19
|
super
|
|
7
20
|
|
|
8
21
|
@timeout = Nonnative::Timeout.new(service.timeout)
|
|
9
22
|
end
|
|
10
23
|
|
|
24
|
+
# Starts the proxy (if any) and spawns the configured process if it is not already running.
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<(Integer, Boolean)>]
|
|
27
|
+
# a tuple of:
|
|
28
|
+
# - the spawned process id (pid)
|
|
29
|
+
# - whether the process appears to still be running (non-blocking wait result)
|
|
11
30
|
def start
|
|
12
31
|
unless process_exists?
|
|
13
32
|
proxy.start
|
|
@@ -18,6 +37,11 @@ module Nonnative
|
|
|
18
37
|
[pid, ::Process.waitpid2(pid, ::Process::WNOHANG).nil?]
|
|
19
38
|
end
|
|
20
39
|
|
|
40
|
+
# Stops the process (if running) and stops the proxy (if any).
|
|
41
|
+
#
|
|
42
|
+
# The process is signalled using the configured signal (defaults to `INT` when not set).
|
|
43
|
+
#
|
|
44
|
+
# @return [Integer, nil] the pid that was stopped (or `nil` if the process was never started)
|
|
21
45
|
def stop
|
|
22
46
|
if process_exists?
|
|
23
47
|
process_kill
|
|
@@ -28,6 +52,11 @@ module Nonnative
|
|
|
28
52
|
pid
|
|
29
53
|
end
|
|
30
54
|
|
|
55
|
+
# Returns a memoized memory reader for the spawned process.
|
|
56
|
+
#
|
|
57
|
+
# This is primarily used by acceptance tests to assert memory usage.
|
|
58
|
+
#
|
|
59
|
+
# @return [GetProcessMem, nil] a memory reader for the pid, or `nil` if not started
|
|
31
60
|
def memory
|
|
32
61
|
return if pid.nil?
|
|
33
62
|
|
data/lib/nonnative/proxy.rb
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Base class for proxy implementations.
|
|
5
|
+
#
|
|
6
|
+
# A proxy is responsible for interposing behavior between a client and a target service.
|
|
7
|
+
# Runners ({Nonnative::Process}, {Nonnative::Server}, and {Nonnative::Service}) create a proxy
|
|
8
|
+
# instance via {Nonnative::ProxyFactory} based on `service.proxy.kind`.
|
|
9
|
+
#
|
|
10
|
+
# Concrete proxies typically implement these public methods:
|
|
11
|
+
# - `start`: begin proxying (bind/listen, start threads, etc)
|
|
12
|
+
# - `stop`: stop proxying and release resources
|
|
13
|
+
# - `reset`: return proxy behavior to a healthy/default state
|
|
14
|
+
# - `host` / `port`: endpoint clients should connect to when the proxy is enabled
|
|
15
|
+
#
|
|
16
|
+
# @see Nonnative::ProxyFactory
|
|
17
|
+
# @see Nonnative::NoProxy
|
|
18
|
+
# @see Nonnative::FaultInjectionProxy
|
|
4
19
|
class Proxy
|
|
20
|
+
# @param service [Nonnative::ConfigurationRunner] runner configuration with an attached proxy configuration
|
|
5
21
|
def initialize(service)
|
|
6
22
|
@service = service
|
|
7
23
|
end
|
|
8
24
|
|
|
9
25
|
protected
|
|
10
26
|
|
|
27
|
+
# Returns the underlying runner configuration.
|
|
28
|
+
#
|
|
29
|
+
# @return [Nonnative::ConfigurationRunner]
|
|
11
30
|
attr_reader :service
|
|
12
31
|
|
|
32
|
+
# Sleeps for the proxy wait interval configured on `service.proxy.wait`.
|
|
33
|
+
#
|
|
34
|
+
# Proxies can use this to allow state transitions to take effect.
|
|
35
|
+
#
|
|
36
|
+
# @return [void]
|
|
13
37
|
def wait
|
|
14
38
|
sleep service.proxy.wait
|
|
15
39
|
end
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Factory for creating proxy instances for runners.
|
|
5
|
+
#
|
|
6
|
+
# Each runtime runner ({Nonnative::Process}, {Nonnative::Server}, {Nonnative::Service}) constructs
|
|
7
|
+
# a proxy via this factory. The proxy implementation is selected by `service.proxy.kind` and resolved
|
|
8
|
+
# using {Nonnative.proxy}.
|
|
9
|
+
#
|
|
10
|
+
# If the kind is unknown (or `"none"`), {Nonnative.proxy} returns {Nonnative::NoProxy}.
|
|
11
|
+
#
|
|
12
|
+
# @see Nonnative.proxy
|
|
13
|
+
# @see Nonnative.proxies
|
|
14
|
+
# @see Nonnative::Proxy
|
|
15
|
+
# @see Nonnative::NoProxy
|
|
4
16
|
class ProxyFactory
|
|
5
17
|
class << self
|
|
18
|
+
# Creates a proxy instance for the given runner configuration.
|
|
19
|
+
#
|
|
20
|
+
# @param service [Nonnative::ConfigurationRunner] runner configuration with an attached proxy configuration
|
|
21
|
+
# @return [Nonnative::Proxy] proxy instance (may be a {Nonnative::NoProxy})
|
|
6
22
|
def create(service)
|
|
7
23
|
proxy = Nonnative.proxy(service.proxy.kind)
|
|
8
24
|
|
data/lib/nonnative/runner.rb
CHANGED
|
@@ -1,26 +1,56 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Base runtime wrapper for a configured runnable unit.
|
|
5
|
+
#
|
|
6
|
+
# A runner wraps a configuration object (a subclass of {Nonnative::ConfigurationRunner}) and
|
|
7
|
+
# exposes lifecycle behavior via specialized subclasses:
|
|
8
|
+
#
|
|
9
|
+
# - {Nonnative::Process} for OS-level child processes
|
|
10
|
+
# - {Nonnative::Server} for in-process Ruby servers (threads)
|
|
11
|
+
# - {Nonnative::Service} for proxy-only external dependencies
|
|
12
|
+
#
|
|
13
|
+
# Each runner has an associated proxy instance created via {Nonnative::ProxyFactory}.
|
|
14
|
+
#
|
|
15
|
+
# @see Nonnative::Process
|
|
16
|
+
# @see Nonnative::Server
|
|
17
|
+
# @see Nonnative::Service
|
|
4
18
|
class Runner
|
|
19
|
+
# Returns the proxy instance for this runner.
|
|
20
|
+
#
|
|
21
|
+
# @return [Nonnative::Proxy]
|
|
5
22
|
attr_reader :proxy
|
|
6
23
|
|
|
24
|
+
# @param service [Nonnative::ConfigurationRunner] runner configuration
|
|
7
25
|
def initialize(service)
|
|
8
26
|
@service = service
|
|
9
27
|
@proxy = Nonnative::ProxyFactory.create(service)
|
|
10
28
|
end
|
|
11
29
|
|
|
30
|
+
# Returns the configured runner name.
|
|
31
|
+
#
|
|
32
|
+
# @return [String, nil]
|
|
12
33
|
def name
|
|
13
34
|
service.name
|
|
14
35
|
end
|
|
15
36
|
|
|
16
37
|
protected
|
|
17
38
|
|
|
39
|
+
# Returns the underlying configuration object.
|
|
40
|
+
#
|
|
41
|
+
# @return [Nonnative::ConfigurationRunner]
|
|
18
42
|
attr_reader :service
|
|
19
43
|
|
|
44
|
+
# Sleeps for the configured `wait` interval after start-related work.
|
|
45
|
+
#
|
|
46
|
+
# @return [void]
|
|
20
47
|
def wait_start
|
|
21
48
|
sleep service.wait
|
|
22
49
|
end
|
|
23
50
|
|
|
51
|
+
# Sleeps for the configured `wait` interval after stop-related work.
|
|
52
|
+
#
|
|
53
|
+
# @return [void]
|
|
24
54
|
def wait_stop
|
|
25
55
|
sleep service.wait
|
|
26
56
|
end
|
data/lib/nonnative/server.rb
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Runtime runner that manages an in-process Ruby server.
|
|
5
|
+
#
|
|
6
|
+
# A server runner:
|
|
7
|
+
# - starts the configured proxy (if any),
|
|
8
|
+
# - starts a Ruby thread that runs {#perform_start},
|
|
9
|
+
# - waits briefly (via the runner `wait`), and
|
|
10
|
+
# - participates in readiness/shutdown via TCP port checks orchestrated by {Nonnative::Pool}.
|
|
11
|
+
#
|
|
12
|
+
# Concrete server implementations are expected to subclass {Nonnative::Server} and implement:
|
|
13
|
+
# - {#perform_start} (to bind/listen and begin serving), and
|
|
14
|
+
# - {#perform_stop} (to gracefully shut down).
|
|
15
|
+
#
|
|
16
|
+
# The underlying configuration is a {Nonnative::ConfigurationServer}.
|
|
17
|
+
#
|
|
18
|
+
# @see Nonnative::ConfigurationServer
|
|
19
|
+
# @see Nonnative::Pool
|
|
4
20
|
class Server < Runner
|
|
21
|
+
# @param service [Nonnative::ConfigurationServer] server configuration
|
|
5
22
|
def initialize(service)
|
|
6
23
|
super
|
|
7
24
|
|
|
8
25
|
@timeout = Nonnative::Timeout.new(service.timeout)
|
|
9
26
|
end
|
|
10
27
|
|
|
28
|
+
# Starts the proxy (if any) and starts the server thread if not already started.
|
|
29
|
+
#
|
|
30
|
+
# @return [Array<(Integer, TrueClass)>]
|
|
31
|
+
# a tuple of:
|
|
32
|
+
# - a stable identifier for this server instance (`object_id`)
|
|
33
|
+
# - `true` (thread creation itself is considered started; readiness is checked separately)
|
|
11
34
|
def start
|
|
12
35
|
unless thread
|
|
13
36
|
proxy.start
|
|
@@ -21,6 +44,11 @@ module Nonnative
|
|
|
21
44
|
[object_id, true]
|
|
22
45
|
end
|
|
23
46
|
|
|
47
|
+
# Stops the server if it is running.
|
|
48
|
+
#
|
|
49
|
+
# Calls {#perform_stop}, terminates the server thread, stops the proxy (if any), and waits briefly.
|
|
50
|
+
#
|
|
51
|
+
# @return [Integer] the server identifier (`object_id`)
|
|
24
52
|
def stop
|
|
25
53
|
if thread
|
|
26
54
|
perform_stop
|
data/lib/nonnative/service.rb
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Runtime runner for an external dependency.
|
|
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).
|
|
9
|
+
#
|
|
10
|
+
# The underlying configuration is a {Nonnative::ConfigurationService}.
|
|
11
|
+
#
|
|
12
|
+
# @see Nonnative::ConfigurationService
|
|
13
|
+
# @see Nonnative::Proxy
|
|
4
14
|
class Service < Runner
|
|
15
|
+
# Starts the configured proxy (if any).
|
|
16
|
+
#
|
|
17
|
+
# @return [void]
|
|
5
18
|
def start
|
|
6
19
|
proxy.start
|
|
7
20
|
|
|
8
21
|
Nonnative.logger.info "started service '#{service.name}'"
|
|
9
22
|
end
|
|
10
23
|
|
|
24
|
+
# Stops the configured proxy (if any).
|
|
25
|
+
#
|
|
26
|
+
# @return [void]
|
|
11
27
|
def stop
|
|
12
28
|
proxy.stop
|
|
13
29
|
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Base socket-pair implementation used by TCP proxies.
|
|
5
|
+
#
|
|
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.
|
|
8
|
+
#
|
|
9
|
+
# This is used by {Nonnative::FaultInjectionProxy} to implement pass-through forwarding, and is
|
|
10
|
+
# subclassed to inject failures (close immediately, delay reads, corrupt writes, etc).
|
|
11
|
+
#
|
|
12
|
+
# The `proxy` argument is expected to provide `host` and `port` for the upstream connection
|
|
13
|
+
# (typically a {Nonnative::ConfigurationProxy}).
|
|
14
|
+
#
|
|
15
|
+
# @see Nonnative::FaultInjectionProxy
|
|
16
|
+
# @see Nonnative::SocketPairFactory
|
|
17
|
+
# @see Nonnative::CloseAllSocketPair
|
|
18
|
+
# @see Nonnative::DelaySocketPair
|
|
19
|
+
# @see Nonnative::InvalidDataSocketPair
|
|
4
20
|
class SocketPair
|
|
21
|
+
# @param proxy [#host, #port, #options] proxy configuration used to connect upstream
|
|
5
22
|
def initialize(proxy)
|
|
6
23
|
@proxy = proxy
|
|
7
24
|
end
|
|
8
25
|
|
|
26
|
+
# Connects the given local socket to an upstream socket and pipes data until the connection ends.
|
|
27
|
+
#
|
|
28
|
+
# @param local_socket [TCPSocket] the accepted client socket
|
|
29
|
+
# @return [void]
|
|
9
30
|
def connect(local_socket)
|
|
10
31
|
remote_socket = create_remote_socket
|
|
11
32
|
|
|
@@ -24,12 +45,24 @@ module Nonnative
|
|
|
24
45
|
|
|
25
46
|
protected
|
|
26
47
|
|
|
48
|
+
# Returns the proxy configuration.
|
|
49
|
+
#
|
|
50
|
+
# @return [Object]
|
|
27
51
|
attr_reader :proxy
|
|
28
52
|
|
|
53
|
+
# Creates the upstream socket connection.
|
|
54
|
+
#
|
|
55
|
+
# @return [TCPSocket]
|
|
29
56
|
def create_remote_socket
|
|
30
57
|
::TCPSocket.new(proxy.host, proxy.port)
|
|
31
58
|
end
|
|
32
59
|
|
|
60
|
+
# Pipes data from `socket1` to `socket2` if `socket1` is readable.
|
|
61
|
+
#
|
|
62
|
+
# @param ready [Array<Array<IO>>] the result from `select`
|
|
63
|
+
# @param socket1 [IO] readable side
|
|
64
|
+
# @param socket2 [IO] writable side
|
|
65
|
+
# @return [Boolean] whether the piping loop should terminate
|
|
33
66
|
def pipe?(ready, socket1, socket2)
|
|
34
67
|
if ready[0].include?(socket1)
|
|
35
68
|
data = read(socket1)
|
|
@@ -41,10 +74,23 @@ module Nonnative
|
|
|
41
74
|
false
|
|
42
75
|
end
|
|
43
76
|
|
|
77
|
+
# Reads bytes from the given socket.
|
|
78
|
+
#
|
|
79
|
+
# Subclasses can override this to inject behavior (e.g. delay).
|
|
80
|
+
#
|
|
81
|
+
# @param socket [IO]
|
|
82
|
+
# @return [String]
|
|
44
83
|
def read(socket)
|
|
45
84
|
socket.recv(1024) || ''
|
|
46
85
|
end
|
|
47
86
|
|
|
87
|
+
# Writes bytes to the given socket.
|
|
88
|
+
#
|
|
89
|
+
# Subclasses can override this to inject behavior (e.g. corrupt data).
|
|
90
|
+
#
|
|
91
|
+
# @param socket [IO]
|
|
92
|
+
# @param data [String]
|
|
93
|
+
# @return [Integer] number of bytes written
|
|
48
94
|
def write(socket, data)
|
|
49
95
|
socket.write(data)
|
|
50
96
|
end
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Factory for creating socket-pair implementations used by {Nonnative::FaultInjectionProxy}.
|
|
5
|
+
#
|
|
6
|
+
# A socket-pair is responsible for wiring a local accepted socket to a remote upstream socket,
|
|
7
|
+
# optionally injecting failures (close connections, add delays, corrupt data, etc).
|
|
8
|
+
#
|
|
9
|
+
# Proxy states are mapped as follows:
|
|
10
|
+
# - `:none` (or any unknown value) -> {Nonnative::SocketPair} (pass-through)
|
|
11
|
+
# - `:close_all` -> {Nonnative::CloseAllSocketPair}
|
|
12
|
+
# - `:delay` -> {Nonnative::DelaySocketPair}
|
|
13
|
+
# - `:invalid_data` -> {Nonnative::InvalidDataSocketPair}
|
|
14
|
+
#
|
|
15
|
+
# @see Nonnative::FaultInjectionProxy
|
|
16
|
+
# @see Nonnative::SocketPair
|
|
17
|
+
# @see Nonnative::CloseAllSocketPair
|
|
18
|
+
# @see Nonnative::DelaySocketPair
|
|
19
|
+
# @see Nonnative::InvalidDataSocketPair
|
|
4
20
|
class SocketPairFactory
|
|
5
21
|
class << self
|
|
22
|
+
# Creates a socket-pair instance for the given proxy state.
|
|
23
|
+
#
|
|
24
|
+
# @param kind [Symbol] proxy state (e.g. `:none`, `:close_all`, `:delay`, `:invalid_data`)
|
|
25
|
+
# @param proxy [Nonnative::ConfigurationProxy] proxy configuration (host/port/options)
|
|
26
|
+
# @return [Nonnative::SocketPair] a socket-pair implementation instance
|
|
6
27
|
def create(kind, proxy)
|
|
7
28
|
pair = case kind
|
|
8
29
|
when :close_all
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Raised when {Nonnative.start} fails to start one or more configured runners.
|
|
5
|
+
#
|
|
6
|
+
# The error message typically contains one line per failing runner.
|
|
7
|
+
#
|
|
8
|
+
# @see Nonnative.start
|
|
4
9
|
class StartError < Nonnative::Error
|
|
5
10
|
end
|
|
6
11
|
end
|
data/lib/nonnative/stop_error.rb
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Raised when {Nonnative.stop} fails to stop one or more configured runners within the configured timeouts.
|
|
5
|
+
#
|
|
6
|
+
# The error message typically contains one line per runner that did not stop cleanly in time.
|
|
7
|
+
#
|
|
8
|
+
# @see Nonnative.stop
|
|
4
9
|
class StopError < Nonnative::Error
|
|
5
10
|
end
|
|
6
11
|
end
|
data/lib/nonnative/timeout.rb
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
+
# Small helper to run a block with a timeout and convert timeout errors into `false`.
|
|
5
|
+
#
|
|
6
|
+
# This is used internally for readiness/shutdown loops (for example port checks) where the common
|
|
7
|
+
# control-flow is “keep retrying until the timeout elapses”.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# timeout = Nonnative::Timeout.new(1) # seconds
|
|
11
|
+
# ok = timeout.perform do
|
|
12
|
+
# # do work that may take time
|
|
13
|
+
# true
|
|
14
|
+
# end
|
|
15
|
+
# # ok is either the block result or false if the timeout elapsed
|
|
16
|
+
#
|
|
4
17
|
class Timeout
|
|
18
|
+
# @param time [Numeric] timeout duration in seconds
|
|
5
19
|
def initialize(time)
|
|
6
20
|
@time = time
|
|
7
21
|
end
|
|
8
22
|
|
|
23
|
+
# Executes the given block with the configured timeout.
|
|
24
|
+
#
|
|
25
|
+
# If the timeout elapses, returns `false` instead of raising `Timeout::Error`.
|
|
26
|
+
#
|
|
27
|
+
# @yield the work to execute under a timeout
|
|
28
|
+
# @return [Object, false] the block's return value, or `false` if the timeout elapsed
|
|
9
29
|
def perform(&)
|
|
10
30
|
::Timeout.timeout(time, &)
|
|
11
31
|
rescue ::Timeout::Error
|
data/lib/nonnative/version.rb
CHANGED
data/lib/nonnative.rb
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# = Nonnative
|
|
4
|
+
#
|
|
5
|
+
# Nonnative is a Ruby-first harness for end-to-end testing of services implemented in other languages.
|
|
6
|
+
# It can:
|
|
7
|
+
#
|
|
8
|
+
# - start external processes and in-process servers
|
|
9
|
+
# - wait for readiness via port checks
|
|
10
|
+
# - optionally run fault-injection proxies in front of services
|
|
11
|
+
#
|
|
12
|
+
# The public entry points are exposed as module-level methods on {Nonnative}.
|
|
13
|
+
#
|
|
14
|
+
# == Basic usage
|
|
15
|
+
#
|
|
16
|
+
# Configure the system under test:
|
|
17
|
+
#
|
|
18
|
+
# Nonnative.configure do |config|
|
|
19
|
+
# config.name = 'example'
|
|
20
|
+
# config.url = 'http://127.0.0.1:8080'
|
|
21
|
+
# config.log = 'test.log'
|
|
22
|
+
#
|
|
23
|
+
# config.process do |p|
|
|
24
|
+
# p.name = 'api'
|
|
25
|
+
# p.command = -> { './bin/api' }
|
|
26
|
+
# p.host = '127.0.0.1'
|
|
27
|
+
# p.port = 8080
|
|
28
|
+
# p.timeout = 10
|
|
29
|
+
# p.log = 'api.log'
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# Start and stop around your test suite:
|
|
34
|
+
#
|
|
35
|
+
# Nonnative.start
|
|
36
|
+
# # run tests...
|
|
37
|
+
# Nonnative.stop
|
|
38
|
+
#
|
|
39
|
+
# == Notes
|
|
40
|
+
#
|
|
41
|
+
# This file also requires integration helpers used by acceptance tests. If you require `nonnative` outside a
|
|
42
|
+
# Cucumber runtime, loading `nonnative/cucumber` may not be desirable for your environment.
|
|
43
|
+
#
|
|
3
44
|
require 'socket'
|
|
4
45
|
require 'timeout'
|
|
5
46
|
require 'yaml'
|
|
@@ -56,46 +97,116 @@ require 'nonnative/go_command'
|
|
|
56
97
|
require 'nonnative/cucumber'
|
|
57
98
|
require 'nonnative/header'
|
|
58
99
|
|
|
100
|
+
# The main namespace for the gem.
|
|
101
|
+
#
|
|
102
|
+
# Most consumers will interact with module-level methods:
|
|
103
|
+
#
|
|
104
|
+
# - {Nonnative.configure} / {Nonnative.configuration}
|
|
105
|
+
# - {Nonnative.start} / {Nonnative.stop}
|
|
106
|
+
# - {Nonnative.clear} / {Nonnative.reset}
|
|
107
|
+
#
|
|
108
|
+
# @see Nonnative::Configuration for the configuration DSL
|
|
109
|
+
# @see Nonnative::Pool for lifecycle orchestration once started
|
|
59
110
|
module Nonnative
|
|
60
111
|
class << self
|
|
112
|
+
# Returns the current runner pool (created on {Nonnative.start}).
|
|
113
|
+
#
|
|
114
|
+
# @return [Nonnative::Pool, nil] the pool instance, or `nil` if not started yet
|
|
61
115
|
attr_reader :pool
|
|
62
116
|
|
|
117
|
+
# Loads one or more configuration files using the `config` gem.
|
|
118
|
+
#
|
|
119
|
+
# This is primarily used by {Nonnative::Configuration#load_file}, but is public for advanced cases.
|
|
120
|
+
#
|
|
121
|
+
# @param files [Array<String>] paths to configuration files
|
|
122
|
+
# @return [Config::Options] the loaded configuration object
|
|
63
123
|
def configurations(*files)
|
|
64
124
|
Config.load_files(files)
|
|
65
125
|
end
|
|
66
126
|
|
|
127
|
+
# Returns the current configuration (memoized).
|
|
128
|
+
#
|
|
129
|
+
# @return [Nonnative::Configuration]
|
|
67
130
|
def configuration
|
|
68
131
|
@configuration ||= Nonnative::Configuration.new
|
|
69
132
|
end
|
|
70
133
|
|
|
134
|
+
# Yields the configuration to a block for programmatic setup.
|
|
135
|
+
#
|
|
136
|
+
# @yieldparam config [Nonnative::Configuration]
|
|
137
|
+
# @return [void]
|
|
138
|
+
#
|
|
139
|
+
# @example
|
|
140
|
+
# Nonnative.configure do |config|
|
|
141
|
+
# config.name = 'my-service'
|
|
142
|
+
# # ...
|
|
143
|
+
# end
|
|
71
144
|
def configure
|
|
72
145
|
yield configuration
|
|
73
146
|
end
|
|
74
147
|
|
|
148
|
+
# Returns the gem logger (memoized).
|
|
149
|
+
#
|
|
150
|
+
# The logger writes to the path configured at {Nonnative::Configuration#log}.
|
|
151
|
+
#
|
|
152
|
+
# @return [Logger]
|
|
75
153
|
def logger
|
|
76
154
|
@logger ||= Logger.new(configuration.log)
|
|
77
155
|
end
|
|
78
156
|
|
|
157
|
+
# Reads a file and returns only lines matching the given predicate.
|
|
158
|
+
#
|
|
159
|
+
# @param path [String] file path to read
|
|
160
|
+
# @param predicate [#call] callable that receives a line and returns truthy/falsey
|
|
161
|
+
# @return [Array<String>] matching lines
|
|
79
162
|
def log_lines(path, predicate)
|
|
80
163
|
File.readlines(path).select { |l| predicate.call(l) }
|
|
81
164
|
end
|
|
82
165
|
|
|
166
|
+
# Builds a Go test executable command line with optional profiling/trace/coverage flags.
|
|
167
|
+
#
|
|
168
|
+
# This is used when process configuration specifies a `go` section.
|
|
169
|
+
#
|
|
170
|
+
# @param tools [Array<String>] enabled tool names (e.g. `["prof", "trace", "cover"]`)
|
|
171
|
+
# @param output [String] directory where outputs should be written
|
|
172
|
+
# @param exec [String] the test binary (or wrapper) to execute
|
|
173
|
+
# @param cmd [String] the command argument passed to the test binary
|
|
174
|
+
# @param params [Array<String>] extra parameters for the command
|
|
175
|
+
# @return [String] executable command string
|
|
83
176
|
def go_executable(tools, output, exec, cmd, *params)
|
|
84
177
|
Nonnative::GoCommand.new(tools, exec, output).executable(cmd, params)
|
|
85
178
|
end
|
|
86
179
|
|
|
180
|
+
# Returns an HTTP client for common health/readiness endpoints.
|
|
181
|
+
#
|
|
182
|
+
# @return [Nonnative::Observability]
|
|
87
183
|
def observability
|
|
88
184
|
@observability ||= Nonnative::Observability.new(configuration.url)
|
|
89
185
|
end
|
|
90
186
|
|
|
187
|
+
# Returns the configured proxy kinds mapped to proxy classes.
|
|
188
|
+
#
|
|
189
|
+
# Consumers can extend this map to add custom proxy implementations.
|
|
190
|
+
#
|
|
191
|
+
# @return [Hash{String=>Class}]
|
|
91
192
|
def proxies
|
|
92
193
|
@proxies ||= { 'fault_injection' => Nonnative::FaultInjectionProxy }.freeze
|
|
93
194
|
end
|
|
94
195
|
|
|
196
|
+
# Resolves a proxy implementation for a configured kind.
|
|
197
|
+
#
|
|
198
|
+
# @param kind [String] proxy kind name (for example `"fault_injection"`)
|
|
199
|
+
# @return [Class] a subclass of {Nonnative::Proxy}
|
|
95
200
|
def proxy(kind)
|
|
96
201
|
Nonnative.proxies[kind] || Nonnative::NoProxy
|
|
97
202
|
end
|
|
98
203
|
|
|
204
|
+
# Starts all configured services, servers, and processes, and waits for readiness.
|
|
205
|
+
#
|
|
206
|
+
# Readiness is determined by attempting to connect to each runner's configured host/port.
|
|
207
|
+
#
|
|
208
|
+
# @return [void]
|
|
209
|
+
# @raise [Nonnative::StartError] if one or more runners fail to start or become ready in time
|
|
99
210
|
def start
|
|
100
211
|
@pool ||= Nonnative::Pool.new(configuration)
|
|
101
212
|
errors = []
|
|
@@ -108,6 +219,10 @@ module Nonnative
|
|
|
108
219
|
raise Nonnative::StartError, errors.join("\n") unless errors.empty?
|
|
109
220
|
end
|
|
110
221
|
|
|
222
|
+
# Stops all configured processes and servers, then services, and waits for shutdown.
|
|
223
|
+
#
|
|
224
|
+
# @return [void]
|
|
225
|
+
# @raise [Nonnative::StopError] if one or more runners fail to stop in time
|
|
111
226
|
def stop
|
|
112
227
|
return if @pool.nil?
|
|
113
228
|
|
|
@@ -120,19 +235,32 @@ module Nonnative
|
|
|
120
235
|
raise Nonnative::StopError, errors.join("\n") unless errors.empty?
|
|
121
236
|
end
|
|
122
237
|
|
|
238
|
+
# Clears the memoized configuration instance.
|
|
239
|
+
#
|
|
240
|
+
# @return [void]
|
|
123
241
|
def clear_configuration
|
|
124
242
|
@configuration = nil
|
|
125
243
|
end
|
|
126
244
|
|
|
245
|
+
# Clears the memoized pool instance.
|
|
246
|
+
#
|
|
247
|
+
# @return [void]
|
|
127
248
|
def clear_pool
|
|
128
249
|
@pool = nil
|
|
129
250
|
end
|
|
130
251
|
|
|
252
|
+
# Clears memoized configuration and pool.
|
|
253
|
+
#
|
|
254
|
+
# @return [void]
|
|
131
255
|
def clear
|
|
132
256
|
clear_configuration
|
|
133
257
|
clear_pool
|
|
134
258
|
end
|
|
135
259
|
|
|
260
|
+
# Resets proxies for all currently started runners.
|
|
261
|
+
#
|
|
262
|
+
# @return [void]
|
|
263
|
+
# @raise [NoMethodError] if called before {Nonnative.start} (because {Nonnative.pool} is nil)
|
|
136
264
|
def reset
|
|
137
265
|
Nonnative.pool.reset
|
|
138
266
|
end
|
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: 1.
|
|
4
|
+
version: 1.108.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alejandro Falkowski
|
|
@@ -264,6 +264,7 @@ files:
|
|
|
264
264
|
- ".gitignore"
|
|
265
265
|
- ".gitmodules"
|
|
266
266
|
- ".rubocop.yml"
|
|
267
|
+
- AGENTS.md
|
|
267
268
|
- CHANGELOG.md
|
|
268
269
|
- Gemfile
|
|
269
270
|
- Gemfile.lock
|
|
@@ -330,7 +331,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
330
331
|
- !ruby/object:Gem::Version
|
|
331
332
|
version: '0'
|
|
332
333
|
requirements: []
|
|
333
|
-
rubygems_version:
|
|
334
|
+
rubygems_version: 4.0.3
|
|
334
335
|
specification_version: 4
|
|
335
336
|
summary: Allows you to keep using the power of ruby to test other systems
|
|
336
337
|
test_files: []
|