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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +8 -6
  3. data/.rubocop.yml +3 -0
  4. data/AGENTS.md +248 -0
  5. data/CHANGELOG.md +104 -0
  6. data/Gemfile.lock +53 -51
  7. data/README.md +51 -32
  8. data/lib/nonnative/close_all_socket_pair.rb +14 -0
  9. data/lib/nonnative/configuration.rb +67 -0
  10. data/lib/nonnative/configuration_process.rb +14 -0
  11. data/lib/nonnative/configuration_proxy.rb +28 -0
  12. data/lib/nonnative/configuration_runner.rb +44 -0
  13. data/lib/nonnative/configuration_server.rb +12 -0
  14. data/lib/nonnative/configuration_service.rb +9 -0
  15. data/lib/nonnative/delay_socket_pair.rb +15 -0
  16. data/lib/nonnative/error.rb +7 -0
  17. data/lib/nonnative/fault_injection_proxy.rb +63 -0
  18. data/lib/nonnative/go_command.rb +34 -0
  19. data/lib/nonnative/grpc_server.rb +30 -0
  20. data/lib/nonnative/header.rb +44 -0
  21. data/lib/nonnative/http_client.rb +45 -0
  22. data/lib/nonnative/http_proxy_server.rb +62 -1
  23. data/lib/nonnative/http_server.rb +40 -0
  24. data/lib/nonnative/invalid_data_socket_pair.rb +15 -0
  25. data/lib/nonnative/no_proxy.rb +35 -0
  26. data/lib/nonnative/not_found_error.rb +7 -0
  27. data/lib/nonnative/observability.rb +44 -0
  28. data/lib/nonnative/pool.rb +50 -0
  29. data/lib/nonnative/port.rb +26 -0
  30. data/lib/nonnative/process.rb +29 -0
  31. data/lib/nonnative/proxy.rb +24 -0
  32. data/lib/nonnative/proxy_factory.rb +16 -0
  33. data/lib/nonnative/runner.rb +30 -0
  34. data/lib/nonnative/server.rb +28 -0
  35. data/lib/nonnative/service.rb +16 -0
  36. data/lib/nonnative/socket_pair.rb +46 -0
  37. data/lib/nonnative/socket_pair_factory.rb +21 -0
  38. data/lib/nonnative/start_error.rb +5 -0
  39. data/lib/nonnative/stop_error.rb +5 -0
  40. data/lib/nonnative/timeout.rb +20 -0
  41. data/lib/nonnative/version.rb +4 -1
  42. data/lib/nonnative.rb +128 -0
  43. metadata +3 -2
data/README.md CHANGED
@@ -5,15 +5,15 @@
5
5
 
6
6
  # Nonnative
7
7
 
8
- Do you love building microservices using different languages?
8
+ Nonnative is a Ruby-first harness for end-to-end testing of systems implemented in other languages.
9
9
 
10
- Do you love testing applications using [cucumber](https://cucumber.io/) with [ruby](https://www.ruby-lang.org/en/)?
10
+ It helps you:
11
+ - start **OS processes** (e.g. your Go/Java/Rust service binary),
12
+ - start **in-process Ruby servers** (e.g. small HTTP/TCP/gRPC fakes for dependencies),
13
+ - optionally start **proxies** in front of processes/servers/services for fault-injection,
14
+ - wait for readiness/shutdown using **TCP port checks**.
11
15
 
12
- Well so do I. The issue is that most languages the cucumber implementation is not always complete or you have to write a lot of code to get it working.
13
-
14
- So why not test the way you want and build the microservice how you want. These kind of tests will make sure your application is tested properly by going end-to-end.
15
-
16
- The way it works is it spawns processes or servers you configure and waits for it to start. Then you communicate with your microservice however you like (TCP, HTTP, gRPC, etc)
16
+ Once started, you can test however you like (TCP, HTTP, gRPC, etc).
17
17
 
18
18
  ## Installation
19
19
 
@@ -37,27 +37,38 @@ gem install nonnative
37
37
 
38
38
  ## Usage
39
39
 
40
- Configure nonnative with the following:
40
+ Nonnative is configured via {#Nonnative.configure} (programmatic) or `config.load_file(...)` (YAML).
41
+
42
+ High-level configuration fields:
43
+ - `version`: configuration version (example: `"1.0"`).
44
+ - `name`: logical system name (used by `Nonnative.observability` for `/<name>/healthz`, etc).
45
+ - `url`: base URL for observability queries (example: `http://localhost:4567`).
46
+ - `log`: path for the Nonnative logger output.
47
+ - `processes`: child processes to `spawn`.
48
+ - `servers`: in-process Ruby servers started in threads.
49
+ - `services`: external dependencies (proxy-only; no process/thread started by Nonnative).
41
50
 
42
- - The version of the configuration (1.0).
43
- - The name of the service.
44
- - The URL of the service.
45
- - A log file.
46
- - Process, Server or Service that you want to start.
47
- - A timeout value.
48
- - A time to wait.
49
- - Port to verify.
50
- - The class for servers.
51
- - The log for servers/processes
52
- - The strategy for processes, servers and services.
51
+ Runner fields (process/server/service):
52
+ - `timeout`: max time (seconds) for readiness/shutdown port checks.
53
+ - `wait`: small sleep (seconds) between lifecycle steps.
54
+ - `host`/`port`: address used for port checks; when a proxy is enabled, reads happen via the proxy.
55
+ - `log`: per-runner log file (used by process output redirection or server implementations).
53
56
 
54
- ### Strategy
57
+ ### Lifecycle strategies (Cucumber integration)
55
58
 
56
- The strategy can be one of the following values:
59
+ Nonnative ships Cucumber hooks (when loaded) that support these tags/strategies:
60
+ - `@startup`: start before scenario; stop after scenario
61
+ - `@manual`: stop after scenario (start is expected to be triggered manually in steps)
62
+ - `@clear`: clears memoized configuration and pool before scenario
63
+ - `@reset`: resets proxies after scenario
57
64
 
58
- - startup - When we include `nonnative/startup`, it will start it once.
59
- - before - When we tag our features with `@startup` it will start and stop after the scenario.
60
- - manual - When we tag our features with `@manual` it will stop after the scenario.
65
+ If you want “start once per test run”, require:
66
+
67
+ ```ruby
68
+ require 'nonnative/startup'
69
+ ```
70
+
71
+ This calls `Nonnative.start` immediately and registers an `at_exit` stop.
61
72
 
62
73
  ### Processes
63
74
 
@@ -443,9 +454,9 @@ end
443
454
 
444
455
  ### Services
445
456
 
446
- A service is an external dependency to your system. This is usually an expensive process to start like a DB and you would prefer to be managed externally. Services are not really exciting by themselves, it's when we add proxies that they allow us to do some extra work.
457
+ A service is an external dependency to your system that you **do not** want Nonnative to start (no OS process, no Ruby thread). Services are primarily useful when paired with proxies, because they let you inject failures into dependencies that are managed elsewhere (e.g. a DB running in Docker).
447
458
 
448
- Setup it up programmatically:
459
+ Set it up programmatically:
449
460
 
450
461
  ```ruby
451
462
  require 'nonnative'
@@ -458,33 +469,37 @@ Nonnative.configure do |config|
458
469
 
459
470
  config.service do |s|
460
471
  s.name = 'postgres'
461
- p.port = 5432
472
+ s.host = '127.0.0.1'
473
+ s.port = 5432
462
474
  end
463
475
 
464
476
  config.service do |s|
465
477
  s.name = 'redis'
478
+ s.host = '127.0.0.1'
466
479
  s.port = 6379
467
480
  end
468
481
  end
469
482
  ```
470
483
 
471
- Setup it up through configuration:
484
+ Set it up through configuration (YAML):
472
485
 
473
486
  ```yaml
474
487
  version: "1.0"
475
488
  name: test
476
489
  url: http://localhost:4567
477
490
  log: nonnative.log
478
- processes:
491
+ services:
479
492
  -
480
493
  name: postgres
494
+ host: 127.0.0.1
481
495
  port: 5432
482
496
  -
483
497
  name: redis
498
+ host: 127.0.0.1
484
499
  port: 6379
485
500
  ```
486
501
 
487
- Then load the file with
502
+ Then load the file with:
488
503
 
489
504
  ```ruby
490
505
  require 'nonnative'
@@ -593,7 +608,7 @@ servers:
593
608
 
594
609
  ##### Proxies Services
595
610
 
596
- Setup it up programmatically:
611
+ Set it up programmatically:
597
612
 
598
613
  ```ruby
599
614
  require 'nonnative'
@@ -603,11 +618,15 @@ Nonnative.configure do |config|
603
618
  config.name = 'test'
604
619
  config.url = 'http://localhost:4567'
605
620
  config.log = 'nonnative.log'
606
- config.wait = 1
607
621
 
608
622
  config.service do |s|
623
+ s.name = 'redis'
624
+ s.host = '127.0.0.1'
625
+ s.port = 6379
626
+
609
627
  s.proxy = {
610
628
  kind: 'fault_injection',
629
+ host: '127.0.0.1',
611
630
  port: 20_000,
612
631
  log: 'proxy_server.log',
613
632
  wait: 1,
@@ -1,7 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Socket-pair variant used by the fault-injection proxy to simulate immediate connection failure.
5
+ #
6
+ # When active, the proxy accepts a TCP connection and closes it immediately without forwarding any
7
+ # bytes to the upstream service.
8
+ #
9
+ # This behavior is enabled by calling {Nonnative::FaultInjectionProxy#close_all}.
10
+ #
11
+ # @see Nonnative::FaultInjectionProxy
12
+ # @see Nonnative::SocketPairFactory
13
+ # @see Nonnative::SocketPair
4
14
  class CloseAllSocketPair < SocketPair
15
+ # Closes the accepted socket immediately.
16
+ #
17
+ # @param local_socket [TCPSocket] the accepted client socket
18
+ # @return [void]
5
19
  def connect(local_socket)
6
20
  Nonnative.logger.info "closing socket '#{local_socket.inspect}' for 'close_all' pair"
7
21
 
@@ -1,15 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # The gem configuration object.
5
+ #
6
+ # You can populate configuration either programmatically via the DSL ({#process}, {#server}, {#service}),
7
+ # or by loading a YAML file via {#load_file}.
8
+ #
9
+ # The configuration is consumed when {Nonnative.start} is called.
10
+ #
11
+ # == Programmatic configuration
12
+ #
13
+ # Nonnative.configure do |config|
14
+ # config.name = 'example'
15
+ # config.url = 'http://127.0.0.1:8080'
16
+ # config.log = 'test.log'
17
+ #
18
+ # config.process do |p|
19
+ # p.name = 'api'
20
+ # p.command = -> { './bin/api' }
21
+ # p.host = '127.0.0.1'
22
+ # p.port = 8080
23
+ # p.timeout = 10
24
+ # p.log = 'api.log'
25
+ # end
26
+ # end
27
+ #
28
+ # == File-based configuration
29
+ #
30
+ # Nonnative.configure do |config|
31
+ # config.load_file('features/configs/processes.yml')
32
+ # end
33
+ #
4
34
  class Configuration
35
+ # Creates an empty configuration.
36
+ #
37
+ # @return [void]
5
38
  def initialize
6
39
  @processes = []
7
40
  @servers = []
8
41
  @services = []
9
42
  end
10
43
 
44
+ # @return [String, nil] logical system name (used for observability endpoints)
45
+ # @return [String, nil] configuration version
46
+ # @return [String, nil] base URL for observability queries (for example `"http://127.0.0.1:8080"`)
47
+ # @return [String, nil] path to the Nonnative log file
48
+ # @return [Array<Nonnative::ConfigurationProcess>] configured processes
49
+ # @return [Array<Nonnative::ConfigurationServer>] configured in-process servers
50
+ # @return [Array<Nonnative::ConfigurationService>] configured services (proxy-only)
11
51
  attr_accessor :name, :version, :url, :log, :processes, :servers, :services
12
52
 
53
+ # Loads a configuration file and appends its runners to this instance.
54
+ #
55
+ # The file is loaded using the `config` gem via {Nonnative.configurations}. Top-level attributes are
56
+ # copied onto this object, and runner sections are transformed into configuration runner objects.
57
+ #
58
+ # @param path [String] path to a configuration file (typically YAML)
59
+ # @return [void]
13
60
  def load_file(path)
14
61
  cfg = Nonnative.configurations(path)
15
62
 
@@ -23,6 +70,10 @@ module Nonnative
23
70
  add_services(cfg)
24
71
  end
25
72
 
73
+ # Adds a process configuration entry.
74
+ #
75
+ # @yieldparam process [Nonnative::ConfigurationProcess]
76
+ # @return [void]
26
77
  def process
27
78
  process = Nonnative::ConfigurationProcess.new
28
79
  yield process
@@ -30,6 +81,10 @@ module Nonnative
30
81
  processes << process
31
82
  end
32
83
 
84
+ # Adds a server configuration entry.
85
+ #
86
+ # @yieldparam server [Nonnative::ConfigurationServer]
87
+ # @return [void]
33
88
  def server
34
89
  server = Nonnative::ConfigurationServer.new
35
90
  yield server
@@ -37,6 +92,13 @@ module Nonnative
37
92
  servers << server
38
93
  end
39
94
 
95
+ # Adds a service configuration entry.
96
+ #
97
+ # A "service" does not manage a Ruby thread or OS process; it exists so that a proxy can be started
98
+ # and controlled for an external dependency.
99
+ #
100
+ # @yieldparam service [Nonnative::ConfigurationService]
101
+ # @return [void]
40
102
  def service
41
103
  service = Nonnative::ConfigurationService.new
42
104
  yield service
@@ -44,6 +106,11 @@ module Nonnative
44
106
  services << service
45
107
  end
46
108
 
109
+ # Finds a configured process by name.
110
+ #
111
+ # @param name [String]
112
+ # @return [Nonnative::ConfigurationProcess]
113
+ # @raise [Nonnative::NotFoundError] if no matching process exists
47
114
  def process_by_name(name)
48
115
  process = processes.find { |s| s.name == name }
49
116
  raise NotFoundError, "Could not find process with name '#{name}'" if process.nil?
@@ -1,7 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Process-specific configuration.
5
+ #
6
+ # A "process" is an OS-level child process started via `spawn` and stopped via signals.
7
+ # It is managed by {Nonnative::Process} at runtime.
8
+ #
9
+ # Instances are usually created through {Nonnative::Configuration#process}.
10
+ #
11
+ # @see Nonnative::Configuration
12
+ # @see Nonnative::Process
4
13
  class ConfigurationProcess < ConfigurationRunner
14
+ # @return [Proc] a callable that returns the command string to execute (e.g. `-> { "./bin/api" }`)
15
+ # @return [String, nil] signal name to use for stopping (defaults to `"INT"` when not set)
16
+ # @return [Numeric] readiness timeout (seconds) used when waiting for the port to open/close
17
+ # @return [String] log file path to append process stdout/stderr to
18
+ # @return [Hash, nil] environment variables to pass to the spawned process
5
19
  attr_accessor :command, :signal, :timeout, :log, :environment
6
20
  end
7
21
  end
@@ -1,9 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Proxy configuration attached to a runner configuration.
5
+ #
6
+ # A proxy allows you to interpose behavior between a client and a real service. For example,
7
+ # the built-in `"fault_injection"` proxy can close connections, introduce delays, or corrupt data
8
+ # for resilience testing.
9
+ #
10
+ # This object is created automatically for each runner via {Nonnative::ConfigurationRunner}.
11
+ # When `kind` is set to `"none"`, no proxy is started and the runner will use its configured
12
+ # `host`/`port` directly.
13
+ #
14
+ # @see Nonnative::ConfigurationRunner#proxy
15
+ # @see Nonnative.proxies
4
16
  class ConfigurationProxy
17
+ # @return [String] proxy kind name (for example `"none"` or `"fault_injection"`)
18
+ # @return [String] proxy bind host (defaults to `"0.0.0.0"`)
19
+ # @return [Integer] proxy bind port (defaults to `0`)
20
+ # @return [String, nil] path to proxy log file (implementation-dependent)
21
+ # @return [Numeric] wait interval (seconds) after proxy state changes (defaults to `0.1`)
22
+ # @return [Hash] proxy implementation options (implementation-dependent)
5
23
  attr_accessor :kind, :host, :port, :log, :wait, :options
6
24
 
25
+ # Creates a proxy configuration with defaults.
26
+ #
27
+ # Defaults:
28
+ # - `kind`: `"none"`
29
+ # - `host`: `"0.0.0.0"`
30
+ # - `port`: `0`
31
+ # - `wait`: `0.1`
32
+ # - `options`: `{}`
33
+ #
34
+ # @return [void]
7
35
  def initialize
8
36
  self.kind = 'none'
9
37
  self.host = '0.0.0.0'
@@ -1,10 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Base configuration for a runnable unit managed by Nonnative.
5
+ #
6
+ # This class holds connection and timing attributes common to processes, servers and services,
7
+ # as well as a nested {Nonnative::ConfigurationProxy} describing how/if a proxy should be started.
8
+ #
9
+ # Instances of this type are typically created via {Nonnative::Configuration#process},
10
+ # {Nonnative::Configuration#server}, or {Nonnative::Configuration#service}.
11
+ #
12
+ # @see Nonnative::ConfigurationProcess
13
+ # @see Nonnative::ConfigurationServer
14
+ # @see Nonnative::ConfigurationService
4
15
  class ConfigurationRunner
16
+ # @return [String, nil] runner name used for lookup (for example via `pool.process_by_name`)
17
+ # @return [String] host to bind/connect to (defaults to `"0.0.0.0"`)
18
+ # @return [Integer] port to bind/connect to
19
+ # @return [Numeric] wait interval (seconds) used by runners between lifecycle steps
5
20
  attr_accessor :name, :host, :port, :wait
21
+
22
+ # Proxy configuration for this runner.
23
+ #
24
+ # Note that this returns a configuration object even if no proxy is enabled; by default
25
+ # the proxy kind is `"none"`.
26
+ #
27
+ # @return [Nonnative::ConfigurationProxy]
6
28
  attr_reader :proxy
7
29
 
30
+ # Creates a runner configuration with defaults.
31
+ #
32
+ # Defaults:
33
+ # - `host`: `"0.0.0.0"`
34
+ # - `port`: `0`
35
+ # - `wait`: `0.1`
36
+ # - `proxy`: a new {Nonnative::ConfigurationProxy} with its own defaults
37
+ #
38
+ # @return [void]
8
39
  def initialize
9
40
  self.host = '0.0.0.0'
10
41
  self.port = 0
@@ -13,6 +44,19 @@ module Nonnative
13
44
  @proxy = Nonnative::ConfigurationProxy.new
14
45
  end
15
46
 
47
+ # Sets proxy configuration using a hash-like value.
48
+ #
49
+ # This is primarily used when loading YAML configuration files, where proxy attributes are
50
+ # represented as scalar values.
51
+ #
52
+ # @param value [Hash] proxy attributes
53
+ # @option value [String] :kind proxy kind name (for example `"fault_injection"`)
54
+ # @option value [String] :host proxy bind host (optional)
55
+ # @option value [Integer] :port proxy bind port
56
+ # @option value [String] :log proxy log file path
57
+ # @option value [Numeric] :wait wait interval (seconds) after state changes (optional)
58
+ # @option value [Hash] :options proxy implementation specific options
59
+ # @return [void]
16
60
  def proxy=(value)
17
61
  proxy.kind = value[:kind]
18
62
  proxy.host = value[:host] if value[:host]
@@ -1,7 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Server-specific configuration.
5
+ #
6
+ # A "server" is an in-process Ruby component started in a background thread and stopped via a
7
+ # server-specific shutdown routine. It is managed by {Nonnative::Server} at runtime.
8
+ #
9
+ # Instances are usually created through {Nonnative::Configuration#server}.
10
+ #
11
+ # @see Nonnative::Configuration
12
+ # @see Nonnative::Server
4
13
  class ConfigurationServer < ConfigurationRunner
14
+ # @return [Class] a class that implements `#initialize(service)`, and lifecycle hooks expected by {Nonnative::Server}
15
+ # @return [Numeric] readiness timeout (seconds) used when waiting for the port to open/close
16
+ # @return [String] log file path used by server implementations (for example Puma/gRPC log files)
5
17
  attr_accessor :klass, :timeout, :log
6
18
  end
7
19
  end
@@ -1,6 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Service-specific configuration.
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.
8
+ #
9
+ # Instances are usually created through {Nonnative::Configuration#service}.
10
+ #
11
+ # @see Nonnative::Configuration
12
+ # @see Nonnative::Service
4
13
  class ConfigurationService < ConfigurationRunner
5
14
  end
6
15
  end
@@ -1,7 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Socket-pair variant used by the fault-injection proxy to simulate slow or stalled connections.
5
+ #
6
+ # When active, reads from the socket are delayed by a configured duration before being forwarded.
7
+ #
8
+ # The delay duration is controlled by `proxy.options[:delay]` and defaults to 2 seconds.
9
+ #
10
+ # This behavior is enabled by calling {Nonnative::FaultInjectionProxy#delay}.
11
+ #
12
+ # @see Nonnative::FaultInjectionProxy
13
+ # @see Nonnative::SocketPairFactory
14
+ # @see Nonnative::SocketPair
4
15
  class DelaySocketPair < SocketPair
16
+ # Reads from the socket after sleeping for the configured delay duration.
17
+ #
18
+ # @param socket [IO] the socket to read from
19
+ # @return [String] the bytes read from the socket
5
20
  def read(socket)
6
21
  Nonnative.logger.info "delaying socket '#{socket.inspect}' for 'delay' pair"
7
22
 
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Base class for all Nonnative errors.
5
+ #
6
+ # Catch this error type if you want to handle any exception raised by this gem.
7
+ #
8
+ # @see Nonnative::StartError
9
+ # @see Nonnative::StopError
10
+ # @see Nonnative::NotFoundError
4
11
  class Error < StandardError
5
12
  end
6
13
  end
@@ -1,7 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Fault-injection proxy for TCP services.
5
+ #
6
+ # This proxy accepts incoming TCP connections and forwards traffic to the configured upstream
7
+ # (`service.proxy.host` / `service.proxy.port`) via a socket-pair implementation. It can also inject
8
+ # failures to help validate client resilience.
9
+ #
10
+ # This class exposes a small public control surface for tests:
11
+ #
12
+ # - {#close_all}: close connections immediately on accept
13
+ # - {#delay}: delay reads by a configured duration (default: 2 seconds)
14
+ # - {#invalid_data}: corrupt outbound data by shuffling characters
15
+ # - {#reset}: return to healthy pass-through behavior
16
+ #
17
+ # State changes terminate any active connections so new connections observe the new behavior.
18
+ #
19
+ # ## Wiring
20
+ #
21
+ # When enabled, your test/client should typically connect to {#host}:{#port} (the proxy endpoint),
22
+ # and the proxy will connect onward to the underlying service.
23
+ #
24
+ # ## Configuration
25
+ #
26
+ # The proxy is configured via the runner’s `proxy` hash:
27
+ #
28
+ # - `kind`: `"fault_injection"`
29
+ # - `host` / `port`: where the proxy should be reached by clients (exposed via {#host}/{#port})
30
+ # - `log`: file path used by this proxy’s internal logger
31
+ # - `wait`: sleep interval (seconds) applied after state changes
32
+ # - `options`:
33
+ # - `delay`: delay duration in seconds used by {#delay}
34
+ #
35
+ # @see Nonnative::Proxy
36
+ # @see Nonnative::SocketPairFactory
4
37
  class FaultInjectionProxy < Nonnative::Proxy
38
+ # @param service [Nonnative::ConfigurationRunner] runner configuration with proxy settings
5
39
  def initialize(service)
6
40
  @connections = Concurrent::Hash.new
7
41
  @logger = Logger.new(service.proxy.log)
@@ -11,6 +45,12 @@ module Nonnative
11
45
  super
12
46
  end
13
47
 
48
+ # Starts the proxy accept loop in a background thread.
49
+ #
50
+ # This binds a TCP server on the underlying runner’s `service.host` / `service.port`.
51
+ # Clients should connect to {#host}:{#port}.
52
+ #
53
+ # @return [void]
14
54
  def start
15
55
  @tcp_server = ::TCPServer.new(service.host, service.port)
16
56
  @thread = Thread.new { perform_start }
@@ -18,6 +58,9 @@ module Nonnative
18
58
  Nonnative.logger.info "started with host '#{service.host}' and port '#{service.port}' for proxy 'fault_injection'"
19
59
  end
20
60
 
61
+ # Stops the proxy and closes its listening socket.
62
+ #
63
+ # @return [void]
21
64
  def stop
22
65
  thread&.terminate
23
66
  tcp_server&.close
@@ -25,26 +68,46 @@ module Nonnative
25
68
  Nonnative.logger.info "stopped with host '#{service.host}' and port '#{service.port}' for proxy 'fault_injection'"
26
69
  end
27
70
 
71
+ # Forces new connections to be closed immediately.
72
+ #
73
+ # @return [void]
28
74
  def close_all
29
75
  apply_state :close_all
30
76
  end
31
77
 
78
+ # Delays reads before forwarding.
79
+ #
80
+ # The delay duration is controlled by `service.proxy.options[:delay]` and defaults to 2 seconds.
81
+ #
82
+ # @return [void]
32
83
  def delay
33
84
  apply_state :delay
34
85
  end
35
86
 
87
+ # Corrupts forwarded data by shuffling characters.
88
+ #
89
+ # @return [void]
36
90
  def invalid_data
37
91
  apply_state :invalid_data
38
92
  end
39
93
 
94
+ # Resets the proxy back to healthy pass-through behavior.
95
+ #
96
+ # @return [void]
40
97
  def reset
41
98
  apply_state :none
42
99
  end
43
100
 
101
+ # Returns the host clients should connect to when using this proxy.
102
+ #
103
+ # @return [String]
44
104
  def host
45
105
  service.proxy.host
46
106
  end
47
107
 
108
+ # Returns the port clients should connect to when using this proxy.
109
+ #
110
+ # @return [Integer]
48
111
  def port
49
112
  service.proxy.port
50
113
  end
@@ -1,13 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nonnative
4
+ # Builds command lines for running a Go test binary with optional profiling/trace/coverage flags.
5
+ #
6
+ # This helper is used by {Nonnative.go_executable} and by YAML configuration when a process has a
7
+ # `go:` section (see {Nonnative::Configuration}).
8
+ #
9
+ # The generated flags use Go's `testing` package flags (e.g. `-test.cpuprofile=...`), so this
10
+ # is intended to run a binary compiled from `go test -c`.
11
+ #
12
+ # ## Tools
13
+ #
14
+ # Tools can be enabled/disabled via the `tools` list. Supported values:
15
+ #
16
+ # - `"prof"`: cpu/mem/block/mutex profiles
17
+ # - `"trace"`: execution trace output
18
+ # - `"cover"`: coverage profile output
19
+ #
20
+ # If `tools` is `nil` or empty, all tools (`prof`, `trace`, `cover`) are enabled.
21
+ #
22
+ # @example
23
+ # cmd = Nonnative::GoCommand.new(%w[prof cover], './svc.test', 'reports')
24
+ # cmd.executable('serve', '--config', 'config.yaml')
25
+ # # => "./svc.test -test.cpuprofile=... -test.coverprofile=... serve --config config.yaml"
26
+ #
27
+ # @see Nonnative.go_executable
4
28
  class GoCommand
29
+ # @param tools [Array<String>, nil] tool names to enable (see class docs)
30
+ # @param exec [String] path to the compiled Go test binary
31
+ # @param output [String] output directory for generated files
5
32
  def initialize(tools, exec, output)
6
33
  @tools = tools.nil? || tools.empty? ? %w[prof trace cover] : tools
7
34
  @exec = exec
8
35
  @output = output
9
36
  end
10
37
 
38
+ # Returns an executable command string including enabled `-test.*` flags.
39
+ #
40
+ # A short random suffix is appended to output filenames to reduce collisions across runs.
41
+ #
42
+ # @param cmd [String] command/sub-command argument passed to the Go test binary
43
+ # @param params [Array<String>] additional parameters passed after `cmd`
44
+ # @return [String] the full command to execute
11
45
  def executable(cmd, *params)
12
46
  params = params.join(' ')
13
47
  "#{exec} #{flags(cmd).join(' ')} #{cmd} #{params}".strip