nonnative 3.0.0 → 3.2.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/Gemfile.lock +1 -1
- data/README.md +40 -197
- data/lib/nonnative/configuration.rb +21 -7
- data/lib/nonnative/configuration_proxy.rb +4 -4
- data/lib/nonnative/configuration_runner.rb +3 -38
- data/lib/nonnative/configuration_service.rb +56 -0
- data/lib/nonnative/cucumber.rb +0 -20
- data/lib/nonnative/fault_injection_proxy.rb +6 -6
- data/lib/nonnative/grpc_server.rb +3 -5
- data/lib/nonnative/http_server.rb +3 -5
- data/lib/nonnative/no_proxy.rb +1 -1
- data/lib/nonnative/pool.rb +1 -3
- data/lib/nonnative/process.rb +2 -6
- data/lib/nonnative/proxy.rb +2 -3
- data/lib/nonnative/proxy_factory.rb +5 -6
- data/lib/nonnative/runner.rb +0 -8
- data/lib/nonnative/server.rb +2 -5
- data/lib/nonnative/service.rb +12 -0
- data/lib/nonnative/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3015cc7b1f784a45dbb77184fd73e67e6cf33ff9d7bb97d09ebd01f1359d85b9
|
|
4
|
+
data.tar.gz: 9bf8f2fc9bb72f8de89e76f44912d62e8a7cd19ccb533c7930600fbc70411b28
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73147dafad8a271673d070f033a1a3f92270994b0593bb7f391d0ae126ed0e34005cfd22af9e01052846bd757b866fba9db4d36a3468219be54208e8121a5f80
|
|
7
|
+
data.tar.gz: ad58bc0015ccdf3f91f9ffbb5ca12de3bd9650d540ef33ec982df353abd6892c496328815f60991e340721971e1bfbf86a663292337a755280d1157c2ff9d5be
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Nonnative is a Ruby-first harness for end-to-end testing of systems implemented
|
|
|
10
10
|
It helps you:
|
|
11
11
|
- start **OS processes** (e.g. your Go/Java/Rust service binary),
|
|
12
12
|
- start **in-process Ruby servers** (e.g. small HTTP/TCP/gRPC fakes for dependencies),
|
|
13
|
-
- optionally start **proxies** in front of
|
|
13
|
+
- optionally start **service proxies** for fault-injection in front of externally managed dependencies,
|
|
14
14
|
- wait for readiness/shutdown using **TCP port checks**.
|
|
15
15
|
|
|
16
16
|
Once started, you can test however you like (TCP, HTTP, gRPC, etc).
|
|
@@ -54,23 +54,22 @@ High-level configuration fields:
|
|
|
54
54
|
- `log`: path for the Nonnative logger output.
|
|
55
55
|
- `processes`: child processes to `spawn`.
|
|
56
56
|
- `servers`: in-process Ruby servers started in threads.
|
|
57
|
-
- `services`: external dependencies (
|
|
57
|
+
- `services`: external dependencies (no process/thread started by Nonnative).
|
|
58
58
|
|
|
59
59
|
Common runner fields:
|
|
60
60
|
- `name`: runner name used for lookup.
|
|
61
|
-
- `host
|
|
61
|
+
- `host`: client-facing host. Defaults to `127.0.0.1`.
|
|
62
62
|
|
|
63
63
|
Process/server fields:
|
|
64
|
+
- `ports`: client-facing ports. These are also used for readiness/shutdown port checks.
|
|
64
65
|
- `timeout`: max time (seconds) for readiness/shutdown port checks.
|
|
65
66
|
- `wait`: small sleep (seconds) between lifecycle steps.
|
|
66
67
|
- `log`: per-runner log file used by process output redirection or server implementations.
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
Service fields:
|
|
70
|
+
- `port`: client-facing service port. Services do not get TCP readiness/shutdown checks from Nonnative.
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
> When a proxy is enabled, tests and clients connect to the runner `host` and first configured `ports` entry; the nested `proxy.host`/`proxy.port` is the upstream target behind the proxy.
|
|
72
|
-
|
|
73
|
-
Nonnative readiness and shutdown checks are TCP-only. Configure ports that are dedicated to the test run; if another process is already listening on the same `host`/`ports` endpoint, results are undefined.
|
|
72
|
+
Nonnative readiness and shutdown checks are TCP-only. Configure process/server ports that are dedicated to the test run; if another process is already listening on the same endpoint, results are undefined.
|
|
74
73
|
|
|
75
74
|
> [!WARNING]
|
|
76
75
|
> Readiness and shutdown checks only prove that a TCP port opened or closed. They do not verify HTTP status, gRPC health, schema readiness, migrations, or application-specific health.
|
|
@@ -248,7 +247,7 @@ module Nonnative
|
|
|
248
247
|
def initialize(service)
|
|
249
248
|
super
|
|
250
249
|
|
|
251
|
-
@socket_server = ::TCPServer.new(
|
|
250
|
+
@socket_server = ::TCPServer.new(service.host, service.port)
|
|
252
251
|
end
|
|
253
252
|
|
|
254
253
|
def perform_start
|
|
@@ -405,9 +404,9 @@ Nonnative.configure do |config|
|
|
|
405
404
|
end
|
|
406
405
|
```
|
|
407
406
|
|
|
408
|
-
##### 🔀 Proxy
|
|
407
|
+
##### 🔀 HTTP Forward Proxy
|
|
409
408
|
|
|
410
|
-
The system allows you to define an HTTP proxy for external systems, e.g. `api.github.com`.
|
|
409
|
+
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.
|
|
411
410
|
|
|
412
411
|
Define your server:
|
|
413
412
|
|
|
@@ -543,9 +542,9 @@ end
|
|
|
543
542
|
|
|
544
543
|
### 🧩 Services
|
|
545
544
|
|
|
546
|
-
A service is an external dependency to your system that you **do not** want Nonnative to start (no OS process, no Ruby thread).
|
|
545
|
+
A service is an external dependency to your system that you **do not** want Nonnative to start (no OS process, no Ruby thread).
|
|
547
546
|
|
|
548
|
-
Services do not get process lifecycle management or TCP readiness/shutdown checks from Nonnative. They
|
|
547
|
+
Services do not get process lifecycle management or TCP readiness/shutdown checks from Nonnative. They provide a named endpoint for a dependency that another tool already manages.
|
|
549
548
|
|
|
550
549
|
Set it up programmatically:
|
|
551
550
|
|
|
@@ -561,13 +560,13 @@ Nonnative.configure do |config|
|
|
|
561
560
|
config.service do |s|
|
|
562
561
|
s.name = 'postgres'
|
|
563
562
|
s.host = '127.0.0.1'
|
|
564
|
-
s.
|
|
563
|
+
s.port = 5432
|
|
565
564
|
end
|
|
566
565
|
|
|
567
566
|
config.service do |s|
|
|
568
567
|
s.name = 'redis'
|
|
569
568
|
s.host = '127.0.0.1'
|
|
570
|
-
s.
|
|
569
|
+
s.port = 6379
|
|
571
570
|
end
|
|
572
571
|
end
|
|
573
572
|
```
|
|
@@ -583,13 +582,11 @@ services:
|
|
|
583
582
|
-
|
|
584
583
|
name: postgres
|
|
585
584
|
host: 127.0.0.1
|
|
586
|
-
|
|
587
|
-
- 5432
|
|
585
|
+
port: 5432
|
|
588
586
|
-
|
|
589
587
|
name: redis
|
|
590
588
|
host: 127.0.0.1
|
|
591
|
-
|
|
592
|
-
- 6379
|
|
589
|
+
port: 6379
|
|
593
590
|
```
|
|
594
591
|
|
|
595
592
|
Then load the file with:
|
|
@@ -618,159 +615,43 @@ Custom proxy kinds can be registered through `Nonnative.proxies`:
|
|
|
618
615
|
Nonnative.proxies['custom'] = CustomProxy
|
|
619
616
|
```
|
|
620
617
|
|
|
621
|
-
For `fault_injection`, keep the
|
|
622
|
-
|
|
623
|
-
##### ⚙️ Process Proxies
|
|
624
|
-
|
|
625
|
-
Add this to an existing process configuration:
|
|
626
|
-
|
|
627
|
-
```ruby
|
|
628
|
-
require 'nonnative'
|
|
629
|
-
|
|
630
|
-
Nonnative.configure do |config|
|
|
631
|
-
config.version = '1.0'
|
|
632
|
-
config.name = 'test'
|
|
633
|
-
config.url = 'http://localhost:4567'
|
|
634
|
-
config.log = 'nonnative.log'
|
|
635
|
-
|
|
636
|
-
config.process do |p|
|
|
637
|
-
p.host = '127.0.0.1'
|
|
638
|
-
p.ports = [20_000]
|
|
639
|
-
|
|
640
|
-
p.proxy = {
|
|
641
|
-
kind: 'fault_injection',
|
|
642
|
-
host: '127.0.0.1',
|
|
643
|
-
port: 12_321,
|
|
644
|
-
log: 'proxy_server.log',
|
|
645
|
-
wait: 1,
|
|
646
|
-
options: {
|
|
647
|
-
delay: 5
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
end
|
|
651
|
-
end
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
YAML fragment:
|
|
618
|
+
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.
|
|
655
619
|
|
|
656
|
-
|
|
657
|
-
version: "1.0"
|
|
658
|
-
name: test
|
|
659
|
-
url: http://localhost:4567
|
|
660
|
-
log: nonnative.log
|
|
661
|
-
processes:
|
|
662
|
-
-
|
|
663
|
-
host: 127.0.0.1
|
|
664
|
-
ports:
|
|
665
|
-
- 20000
|
|
666
|
-
proxy:
|
|
667
|
-
kind: fault_injection
|
|
668
|
-
host: 127.0.0.1
|
|
669
|
-
port: 12321
|
|
670
|
-
log: proxy_server.log
|
|
671
|
-
wait: 1
|
|
672
|
-
options:
|
|
673
|
-
delay: 5
|
|
674
|
-
```
|
|
620
|
+
##### 🧩 Service Proxies
|
|
675
621
|
|
|
676
|
-
|
|
622
|
+
###### Programmatic Configuration
|
|
677
623
|
|
|
678
|
-
Add
|
|
624
|
+
Add a proxy to a service configuration:
|
|
679
625
|
|
|
680
626
|
```ruby
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
kind: 'fault_injection',
|
|
695
|
-
host: '127.0.0.1',
|
|
696
|
-
port: 12_321,
|
|
697
|
-
log: 'proxy_server.log',
|
|
698
|
-
wait: 1,
|
|
699
|
-
options: {
|
|
700
|
-
delay: 5
|
|
701
|
-
}
|
|
627
|
+
config.service do |s|
|
|
628
|
+
s.name = 'redis'
|
|
629
|
+
s.host = '127.0.0.1'
|
|
630
|
+
s.port = 16_379
|
|
631
|
+
|
|
632
|
+
s.proxy = {
|
|
633
|
+
kind: 'fault_injection',
|
|
634
|
+
host: '127.0.0.1',
|
|
635
|
+
port: 6379,
|
|
636
|
+
log: 'proxy_server.log',
|
|
637
|
+
wait: 1,
|
|
638
|
+
options: {
|
|
639
|
+
delay: 5
|
|
702
640
|
}
|
|
703
|
-
|
|
641
|
+
}
|
|
704
642
|
end
|
|
705
643
|
```
|
|
706
644
|
|
|
707
|
-
YAML
|
|
708
|
-
|
|
709
|
-
```yaml
|
|
710
|
-
version: "1.0"
|
|
711
|
-
name: test
|
|
712
|
-
url: http://localhost:4567
|
|
713
|
-
log: nonnative.log
|
|
714
|
-
servers:
|
|
715
|
-
-
|
|
716
|
-
host: 127.0.0.1
|
|
717
|
-
ports:
|
|
718
|
-
- 20000
|
|
719
|
-
proxy:
|
|
720
|
-
kind: fault_injection
|
|
721
|
-
host: 127.0.0.1
|
|
722
|
-
port: 12321
|
|
723
|
-
log: proxy_server.log
|
|
724
|
-
wait: 1
|
|
725
|
-
options:
|
|
726
|
-
delay: 5
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
##### 🧩 Service Proxies
|
|
730
|
-
|
|
731
|
-
Add this to an existing service configuration:
|
|
732
|
-
|
|
733
|
-
```ruby
|
|
734
|
-
require 'nonnative'
|
|
735
|
-
|
|
736
|
-
Nonnative.configure do |config|
|
|
737
|
-
config.version = '1.0'
|
|
738
|
-
config.name = 'test'
|
|
739
|
-
config.url = 'http://localhost:4567'
|
|
740
|
-
config.log = 'nonnative.log'
|
|
741
|
-
|
|
742
|
-
config.service do |s|
|
|
743
|
-
s.name = 'redis'
|
|
744
|
-
s.host = '127.0.0.1'
|
|
745
|
-
s.ports = [16_379]
|
|
746
|
-
|
|
747
|
-
s.proxy = {
|
|
748
|
-
kind: 'fault_injection',
|
|
749
|
-
host: '127.0.0.1',
|
|
750
|
-
port: 6379,
|
|
751
|
-
log: 'proxy_server.log',
|
|
752
|
-
wait: 1,
|
|
753
|
-
options: {
|
|
754
|
-
delay: 5
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
end
|
|
758
|
-
end
|
|
759
|
-
```
|
|
645
|
+
###### YAML Configuration
|
|
760
646
|
|
|
761
|
-
YAML
|
|
647
|
+
Add a proxy to a service YAML entry:
|
|
762
648
|
|
|
763
649
|
```yaml
|
|
764
|
-
version: "1.0"
|
|
765
|
-
name: test
|
|
766
|
-
url: http://localhost:4567
|
|
767
|
-
log: nonnative.log
|
|
768
650
|
services:
|
|
769
651
|
-
|
|
770
652
|
name: redis
|
|
771
653
|
host: 127.0.0.1
|
|
772
|
-
|
|
773
|
-
- 16379
|
|
654
|
+
port: 16379
|
|
774
655
|
proxy:
|
|
775
656
|
kind: fault_injection
|
|
776
657
|
host: 127.0.0.1
|
|
@@ -785,53 +666,15 @@ services:
|
|
|
785
666
|
|
|
786
667
|
The `fault_injection` proxy allows you to simulate failures by injecting them. We currently support the following:
|
|
787
668
|
|
|
788
|
-
Clients connect to the
|
|
669
|
+
Clients connect to the service `host`/`port`, while the proxy forwards traffic to nested `proxy.host`/`proxy.port`.
|
|
789
670
|
|
|
790
671
|
- `close_all` - Closes the socket as soon as it connects.
|
|
791
672
|
- `delay` - Delays traffic on the connection. Defaults to 2 seconds and can be configured through options.
|
|
792
673
|
- `invalid_data` - Forwards client requests unchanged, then corrupts upstream responses before they reach the client.
|
|
793
674
|
|
|
794
|
-
###### ⚙️ Fault Injection Processes
|
|
795
|
-
|
|
796
|
-
Set it up programmatically:
|
|
797
|
-
|
|
798
|
-
```ruby
|
|
799
|
-
name = 'name of process in configuration'
|
|
800
|
-
server = Nonnative.pool.process_by_name(name)
|
|
801
|
-
|
|
802
|
-
server.proxy.close_all # To use close_all.
|
|
803
|
-
server.proxy.reset # To reset it back to a good state.
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
With cucumber:
|
|
807
|
-
|
|
808
|
-
```cucumber
|
|
809
|
-
Given I set the proxy for process 'process_1' to 'close_all'
|
|
810
|
-
Then I should reset the proxy for process 'process_1'
|
|
811
|
-
```
|
|
812
|
-
|
|
813
|
-
###### 🖥️ Fault Injection Servers
|
|
814
|
-
|
|
815
|
-
Set it up programmatically:
|
|
816
|
-
|
|
817
|
-
```ruby
|
|
818
|
-
name = 'name of server in configuration'
|
|
819
|
-
server = Nonnative.pool.server_by_name(name)
|
|
820
|
-
|
|
821
|
-
server.proxy.close_all # To use close_all.
|
|
822
|
-
server.proxy.reset # To reset it back to a good state.
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
With cucumber:
|
|
826
|
-
|
|
827
|
-
```cucumber
|
|
828
|
-
Given I set the proxy for server 'server_1' to 'close_all'
|
|
829
|
-
Then I should reset the proxy for server 'server_1'
|
|
830
|
-
```
|
|
831
|
-
|
|
832
675
|
###### 🧩 Fault Injection Services
|
|
833
676
|
|
|
834
|
-
Set
|
|
677
|
+
Set the proxy state programmatically:
|
|
835
678
|
|
|
836
679
|
```ruby
|
|
837
680
|
name = 'name of service in configuration'
|
|
@@ -841,7 +684,7 @@ service.proxy.close_all # To use close_all.
|
|
|
841
684
|
service.proxy.reset # To reset it back to a good state.
|
|
842
685
|
```
|
|
843
686
|
|
|
844
|
-
|
|
687
|
+
Use the Cucumber proxy steps:
|
|
845
688
|
|
|
846
689
|
```cucumber
|
|
847
690
|
Given I set the proxy for service 'service_1' to 'close_all'
|
|
@@ -124,13 +124,13 @@ module Nonnative
|
|
|
124
124
|
def add_processes(cfg)
|
|
125
125
|
processes = cfg.processes || []
|
|
126
126
|
processes.each do |loaded_process|
|
|
127
|
+
reject_proxy(loaded_process, 'processes')
|
|
128
|
+
|
|
127
129
|
process do |process_config|
|
|
128
130
|
process_config.command = command(loaded_process)
|
|
129
131
|
process_config.signal = loaded_process.signal
|
|
130
132
|
process_config.environment = loaded_process.environment
|
|
131
133
|
runner_attributes(process_config, loaded_process)
|
|
132
|
-
|
|
133
|
-
assign_proxy(process_config, loaded_process.proxy)
|
|
134
134
|
end
|
|
135
135
|
end
|
|
136
136
|
end
|
|
@@ -150,11 +150,11 @@ module Nonnative
|
|
|
150
150
|
def add_servers(cfg)
|
|
151
151
|
servers = cfg.servers || []
|
|
152
152
|
servers.each do |loaded_server|
|
|
153
|
+
reject_proxy(loaded_server, 'servers')
|
|
154
|
+
|
|
153
155
|
server do |server_config|
|
|
154
156
|
server_config.klass = Object.const_get(server_class_name(loaded_server))
|
|
155
157
|
runner_attributes(server_config, loaded_server)
|
|
156
|
-
|
|
157
|
-
assign_proxy(server_config, loaded_server.proxy)
|
|
158
158
|
end
|
|
159
159
|
end
|
|
160
160
|
end
|
|
@@ -165,7 +165,7 @@ module Nonnative
|
|
|
165
165
|
service do |service_config|
|
|
166
166
|
service_config.name = loaded_service.name
|
|
167
167
|
service_config.host = loaded_service.host if loaded_service.host
|
|
168
|
-
|
|
168
|
+
assign_service_port(service_config, loaded_service)
|
|
169
169
|
|
|
170
170
|
assign_proxy(service_config, loaded_service.proxy)
|
|
171
171
|
end
|
|
@@ -193,7 +193,21 @@ module Nonnative
|
|
|
193
193
|
runner.ports = loaded.ports if loaded.ports
|
|
194
194
|
end
|
|
195
195
|
|
|
196
|
-
def
|
|
196
|
+
def assign_service_port(service, loaded)
|
|
197
|
+
values = loaded.to_h
|
|
198
|
+
raise ArgumentError, "Use 'port' instead of 'ports' for service '#{loaded.name}'" if values.key?(:ports) || values.key?('ports')
|
|
199
|
+
|
|
200
|
+
service.port = loaded.port if loaded.port
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def reject_proxy(loaded, kind)
|
|
204
|
+
values = loaded.to_h
|
|
205
|
+
return unless values.key?(:proxy) || values.key?('proxy')
|
|
206
|
+
|
|
207
|
+
raise ArgumentError, "Use 'services' for proxy configuration; #{kind} do not support 'proxy'"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def assign_proxy(service, loaded_proxy)
|
|
197
211
|
return unless loaded_proxy
|
|
198
212
|
|
|
199
213
|
proxy_attributes = {
|
|
@@ -206,7 +220,7 @@ module Nonnative
|
|
|
206
220
|
proxy_attributes[:host] = loaded_proxy.host if loaded_proxy.host
|
|
207
221
|
proxy_attributes[:wait] = loaded_proxy.wait if loaded_proxy.wait
|
|
208
222
|
|
|
209
|
-
|
|
223
|
+
service.proxy = proxy_attributes
|
|
210
224
|
end
|
|
211
225
|
end
|
|
212
226
|
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
-
# Proxy configuration attached to a
|
|
4
|
+
# Proxy configuration attached to a service configuration.
|
|
5
5
|
#
|
|
6
6
|
# A proxy allows you to interpose behavior between a client and a real service. For example,
|
|
7
7
|
# the built-in `"fault_injection"` proxy can close connections, introduce delays, or corrupt data
|
|
8
8
|
# for resilience testing.
|
|
9
9
|
#
|
|
10
|
-
# This object is created automatically for each
|
|
11
|
-
# When `kind` is set to `"none"`, no proxy is started and the
|
|
10
|
+
# This object is created automatically for each service via {Nonnative::ConfigurationService}.
|
|
11
|
+
# When `kind` is set to `"none"`, no proxy is started and the service will use its configured
|
|
12
12
|
# `host`/`port` directly.
|
|
13
13
|
#
|
|
14
|
-
# @see Nonnative::
|
|
14
|
+
# @see Nonnative::ConfigurationService#proxy
|
|
15
15
|
# @see Nonnative.proxies
|
|
16
16
|
class ConfigurationProxy
|
|
17
17
|
# @return [String] proxy kind name (for example `"none"` or `"fault_injection"`)
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
module Nonnative
|
|
4
4
|
# Base configuration for a runnable unit managed by Nonnative.
|
|
5
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.
|
|
6
|
+
# This class holds connection and timing attributes common to processes, servers and services.
|
|
8
7
|
#
|
|
9
8
|
# Instances of this type are typically created via {Nonnative::Configuration#process},
|
|
10
9
|
# {Nonnative::Configuration#server}, or {Nonnative::Configuration#service}.
|
|
@@ -22,29 +21,18 @@ module Nonnative
|
|
|
22
21
|
# @return [Array<Integer>] client-facing ports used for readiness/shutdown checks
|
|
23
22
|
attr_reader :ports
|
|
24
23
|
|
|
25
|
-
# Proxy configuration for this runner.
|
|
26
|
-
#
|
|
27
|
-
# Note that this returns a configuration object even if no proxy is enabled; by default
|
|
28
|
-
# the proxy kind is `"none"`.
|
|
29
|
-
#
|
|
30
|
-
# @return [Nonnative::ConfigurationProxy]
|
|
31
|
-
attr_reader :proxy
|
|
32
|
-
|
|
33
24
|
# Creates a runner configuration with defaults.
|
|
34
25
|
#
|
|
35
26
|
# Defaults:
|
|
36
27
|
# - `host`: `"127.0.0.1"`
|
|
37
28
|
# - `ports`: `[0]`
|
|
38
29
|
# - `wait`: `0.1`
|
|
39
|
-
# - `proxy`: a new {Nonnative::ConfigurationProxy} with its own defaults
|
|
40
30
|
#
|
|
41
31
|
# @return [void]
|
|
42
32
|
def initialize
|
|
43
33
|
self.host = '127.0.0.1'
|
|
44
|
-
|
|
34
|
+
@ports = [0]
|
|
45
35
|
self.wait = 0.1
|
|
46
|
-
|
|
47
|
-
@proxy = Nonnative::ConfigurationProxy.new
|
|
48
36
|
end
|
|
49
37
|
|
|
50
38
|
# Sets the client-facing ports for this runner.
|
|
@@ -57,34 +45,11 @@ module Nonnative
|
|
|
57
45
|
|
|
58
46
|
# Returns the primary client-facing port.
|
|
59
47
|
#
|
|
60
|
-
# This preserves a single endpoint for
|
|
61
|
-
# configuration contract uses {#ports}.
|
|
48
|
+
# This preserves a single endpoint for client helpers while the public configuration contract uses {#ports}.
|
|
62
49
|
#
|
|
63
50
|
# @return [Integer]
|
|
64
51
|
def port
|
|
65
52
|
ports.first
|
|
66
53
|
end
|
|
67
|
-
|
|
68
|
-
# Sets proxy configuration using a hash-like value.
|
|
69
|
-
#
|
|
70
|
-
# This is primarily used when loading YAML configuration files, where proxy attributes are
|
|
71
|
-
# represented as scalar values.
|
|
72
|
-
#
|
|
73
|
-
# @param value [Hash] proxy attributes
|
|
74
|
-
# @option value [String] :kind proxy kind name (for example `"fault_injection"`)
|
|
75
|
-
# @option value [String] :host upstream host behind the proxy (optional)
|
|
76
|
-
# @option value [Integer] :port upstream port behind the proxy
|
|
77
|
-
# @option value [String] :log proxy log file path
|
|
78
|
-
# @option value [Numeric] :wait wait interval (seconds) after state changes (optional)
|
|
79
|
-
# @option value [Hash] :options proxy implementation specific options
|
|
80
|
-
# @return [void]
|
|
81
|
-
def proxy=(value)
|
|
82
|
-
proxy.kind = value[:kind]
|
|
83
|
-
proxy.host = value[:host] if value[:host]
|
|
84
|
-
proxy.port = value[:port]
|
|
85
|
-
proxy.log = value[:log]
|
|
86
|
-
proxy.wait = value[:wait] if value[:wait]
|
|
87
|
-
proxy.options = value[:options]
|
|
88
|
-
end
|
|
89
54
|
end
|
|
90
55
|
end
|
|
@@ -11,5 +11,61 @@ module Nonnative
|
|
|
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
|
|
15
|
+
attr_accessor :port
|
|
16
|
+
|
|
17
|
+
# Proxy configuration for this service.
|
|
18
|
+
#
|
|
19
|
+
# @return [Nonnative::ConfigurationProxy]
|
|
20
|
+
attr_reader :proxy
|
|
21
|
+
|
|
22
|
+
# Creates a service configuration with defaults.
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def initialize
|
|
26
|
+
super
|
|
27
|
+
|
|
28
|
+
self.port = 0
|
|
29
|
+
@proxy = Nonnative::ConfigurationProxy.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Sets proxy configuration using a hash-like value.
|
|
33
|
+
#
|
|
34
|
+
# This is primarily used when loading YAML configuration files, where proxy attributes are
|
|
35
|
+
# represented as scalar values.
|
|
36
|
+
#
|
|
37
|
+
# @param value [Hash] proxy attributes
|
|
38
|
+
# @option value [String] :kind proxy kind name (for example `"fault_injection"`)
|
|
39
|
+
# @option value [String] :host upstream host behind the proxy (optional)
|
|
40
|
+
# @option value [Integer] :port upstream port behind the proxy
|
|
41
|
+
# @option value [String] :log proxy log file path
|
|
42
|
+
# @option value [Numeric] :wait wait interval (seconds) after state changes (optional)
|
|
43
|
+
# @option value [Hash] :options proxy implementation specific options
|
|
44
|
+
# @return [void]
|
|
45
|
+
def proxy=(value)
|
|
46
|
+
proxy.kind = value[:kind]
|
|
47
|
+
proxy.host = value[:host] if value[:host]
|
|
48
|
+
proxy.port = value[:port]
|
|
49
|
+
proxy.log = value[:log]
|
|
50
|
+
proxy.wait = value[:wait] if value[:wait]
|
|
51
|
+
proxy.options = value[:options]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Services expose a single proxy listener, so plural runner ports are not supported.
|
|
55
|
+
#
|
|
56
|
+
# @return [void]
|
|
57
|
+
# @raise [ArgumentError] when plural service ports are read
|
|
58
|
+
def ports
|
|
59
|
+
raise ArgumentError, "Use 'port' instead of 'ports' for service '#{name}'"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Services expose a single proxy listener, so plural runner ports are not supported.
|
|
63
|
+
#
|
|
64
|
+
# @param _value [Array<Integer>] ignored plural ports
|
|
65
|
+
# @return [void]
|
|
66
|
+
# @raise [ArgumentError] when plural service ports are assigned
|
|
67
|
+
def ports=(_value)
|
|
68
|
+
raise ArgumentError, "Use 'port' instead of 'ports' for service '#{name}'"
|
|
69
|
+
end
|
|
14
70
|
end
|
|
15
71
|
end
|
data/lib/nonnative/cucumber.rb
CHANGED
|
@@ -45,16 +45,6 @@ module Nonnative
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def install_proxy_mutation_steps
|
|
48
|
-
Given('I set the proxy for process {string} to {string}') do |name, operation|
|
|
49
|
-
process = Nonnative.pool.process_by_name(name)
|
|
50
|
-
Nonnative::Cucumber::Registration.apply_proxy_operation(process.proxy, operation)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
Given('I set the proxy for server {string} to {string}') do |name, operation|
|
|
54
|
-
server = Nonnative.pool.server_by_name(name)
|
|
55
|
-
Nonnative::Cucumber::Registration.apply_proxy_operation(server.proxy, operation)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
48
|
Given('I set the proxy for service {string} to {string}') do |name, operation|
|
|
59
49
|
service = Nonnative.pool.service_by_name(name)
|
|
60
50
|
Nonnative::Cucumber::Registration.apply_proxy_operation(service.proxy, operation)
|
|
@@ -62,16 +52,6 @@ module Nonnative
|
|
|
62
52
|
end
|
|
63
53
|
|
|
64
54
|
def install_proxy_reset_steps
|
|
65
|
-
Then('I should reset the proxy for process {string}') do |name|
|
|
66
|
-
process = Nonnative.pool.process_by_name(name)
|
|
67
|
-
process.proxy.reset
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
Then('I should reset the proxy for server {string}') do |name|
|
|
71
|
-
server = Nonnative.pool.server_by_name(name)
|
|
72
|
-
server.proxy.reset
|
|
73
|
-
end
|
|
74
|
-
|
|
75
55
|
Then('I should reset the proxy for service {string}') do |name|
|
|
76
56
|
service = Nonnative.pool.service_by_name(name)
|
|
77
57
|
service.proxy.reset
|
|
@@ -18,12 +18,12 @@ module Nonnative
|
|
|
18
18
|
#
|
|
19
19
|
# ## Wiring
|
|
20
20
|
#
|
|
21
|
-
# When enabled, your test/client should connect to the
|
|
22
|
-
#
|
|
21
|
+
# When enabled, your test/client should connect to the service `host` and `port` (the proxy
|
|
22
|
+
# endpoint), and the proxy will forward traffic to the upstream target exposed by {#host}:{#port}.
|
|
23
23
|
#
|
|
24
24
|
# ## Configuration
|
|
25
25
|
#
|
|
26
|
-
# The proxy is configured via the
|
|
26
|
+
# The proxy is configured via the service's `proxy` hash:
|
|
27
27
|
#
|
|
28
28
|
# - `kind`: `"fault_injection"`
|
|
29
29
|
# - `host` / `port`: upstream target behind the proxy (exposed via {#host}/{#port})
|
|
@@ -50,7 +50,7 @@ module Nonnative
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
# @param service [Nonnative::
|
|
53
|
+
# @param service [Nonnative::ConfigurationService] service configuration with proxy settings
|
|
54
54
|
def initialize(service)
|
|
55
55
|
@connections = Concurrent::Hash.new
|
|
56
56
|
@logger = Logger.new(service.proxy.log)
|
|
@@ -62,8 +62,8 @@ module Nonnative
|
|
|
62
62
|
|
|
63
63
|
# Starts the proxy accept loop in a background thread.
|
|
64
64
|
#
|
|
65
|
-
# This binds a TCP server on the
|
|
66
|
-
# Clients connect to that
|
|
65
|
+
# This binds a TCP server on the service `host` and `port`.
|
|
66
|
+
# Clients connect to that service endpoint, while upstream traffic is forwarded to {#host}:{#port}.
|
|
67
67
|
#
|
|
68
68
|
# @return [void]
|
|
69
69
|
def start
|
|
@@ -4,8 +4,7 @@ module Nonnative
|
|
|
4
4
|
# gRPC server runner implemented using {GRPC::RpcServer}.
|
|
5
5
|
#
|
|
6
6
|
# This is a convenience server implementation for running a gRPC service in-process under
|
|
7
|
-
# Nonnative's server lifecycle. It binds to the configured
|
|
8
|
-
# by {Nonnative::Server} via {#perform_start} / {#perform_stop}.
|
|
7
|
+
# Nonnative's server lifecycle. It binds to the configured server `host` and first `ports` entry.
|
|
9
8
|
#
|
|
10
9
|
# Important note about logging: the `grpc` gem uses a global logger. This implementation sets
|
|
11
10
|
# `GRPC.logger` to write to the configured `service.log`, and whichever gRPC server is initialized
|
|
@@ -33,12 +32,11 @@ module Nonnative
|
|
|
33
32
|
|
|
34
33
|
# Binds the gRPC server and begins serving requests.
|
|
35
34
|
#
|
|
36
|
-
# The server binds to the
|
|
37
|
-
# runner host and first configured port as the client-facing endpoint used by readiness checks.
|
|
35
|
+
# The server binds to the configured server host and first configured port.
|
|
38
36
|
#
|
|
39
37
|
# @return [void]
|
|
40
38
|
def perform_start
|
|
41
|
-
server.add_http2_port("#{
|
|
39
|
+
server.add_http2_port("#{service.host}:#{service.port}", :this_port_is_insecure)
|
|
42
40
|
server.run
|
|
43
41
|
end
|
|
44
42
|
|
|
@@ -4,8 +4,7 @@ module Nonnative
|
|
|
4
4
|
# Puma-based HTTP server runner.
|
|
5
5
|
#
|
|
6
6
|
# This is a convenience server implementation for running a Rack/Sinatra application in-process
|
|
7
|
-
# under Nonnative's server lifecycle. It binds to the configured
|
|
8
|
-
# consistently with proxy configuration) and uses Puma for HTTP serving.
|
|
7
|
+
# under Nonnative's server lifecycle. It binds to the configured server `host` and first `ports` entry.
|
|
9
8
|
#
|
|
10
9
|
# The server is started and stopped by {Nonnative::Server} via {#perform_start} / {#perform_stop}.
|
|
11
10
|
#
|
|
@@ -48,12 +47,11 @@ module Nonnative
|
|
|
48
47
|
|
|
49
48
|
# Binds the Puma server and begins serving.
|
|
50
49
|
#
|
|
51
|
-
# The listener binds to the
|
|
52
|
-
# runner host and first configured port as the client-facing endpoint used by readiness checks.
|
|
50
|
+
# The listener binds to the configured server host and first configured port.
|
|
53
51
|
#
|
|
54
52
|
# @return [void]
|
|
55
53
|
def perform_start
|
|
56
|
-
server.add_tcp_listener
|
|
54
|
+
server.add_tcp_listener service.host, service.port
|
|
57
55
|
server.run false
|
|
58
56
|
end
|
|
59
57
|
|
data/lib/nonnative/no_proxy.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Nonnative
|
|
|
7
7
|
# It does not bind/listen or alter traffic; it simply exposes the underlying runner's configured
|
|
8
8
|
# `host` and primary `port`.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# Services can always call `start`, `stop`, and `reset` safely on this proxy.
|
|
11
11
|
#
|
|
12
12
|
# @see Nonnative.proxy
|
|
13
13
|
# @see Nonnative::Proxy
|
data/lib/nonnative/pool.rb
CHANGED
|
@@ -104,15 +104,13 @@ module Nonnative
|
|
|
104
104
|
services[runner_index(configuration.services, name)]
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
# Resets proxies
|
|
107
|
+
# Resets service proxies in this pool.
|
|
108
108
|
#
|
|
109
109
|
# This is used by the Cucumber `@reset` hook and is safe to call any time after the pool is created.
|
|
110
110
|
#
|
|
111
111
|
# @return [void]
|
|
112
112
|
def reset
|
|
113
113
|
services.each { |s| s.proxy.reset }
|
|
114
|
-
servers.each { |s| s.first.proxy.reset }
|
|
115
|
-
processes.each { |p| p.first.proxy.reset }
|
|
116
114
|
end
|
|
117
115
|
|
|
118
116
|
private
|
data/lib/nonnative/process.rb
CHANGED
|
@@ -4,7 +4,6 @@ module Nonnative
|
|
|
4
4
|
# Runtime runner that manages an OS-level child process.
|
|
5
5
|
#
|
|
6
6
|
# A process runner:
|
|
7
|
-
# - starts the configured proxy (if any),
|
|
8
7
|
# - spawns a child process using the configured command and environment,
|
|
9
8
|
# - waits briefly (via the runner `wait`), and
|
|
10
9
|
# - participates in readiness/shutdown via TCP port checks orchestrated by {Nonnative::Pool}.
|
|
@@ -21,7 +20,7 @@ module Nonnative
|
|
|
21
20
|
@timeout = Nonnative::Timeout.new(service.timeout)
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
#
|
|
23
|
+
# Spawns the configured process if it is not already running.
|
|
25
24
|
#
|
|
26
25
|
# @return [Array<(Integer, Boolean)>]
|
|
27
26
|
# a tuple of:
|
|
@@ -29,7 +28,6 @@ module Nonnative
|
|
|
29
28
|
# - whether the process appears to still be running (non-blocking wait result)
|
|
30
29
|
def start
|
|
31
30
|
unless process_exists?
|
|
32
|
-
proxy.start
|
|
33
31
|
@pid = process_spawn
|
|
34
32
|
wait_start
|
|
35
33
|
end
|
|
@@ -37,7 +35,7 @@ module Nonnative
|
|
|
37
35
|
[pid, ::Process.waitpid2(pid, ::Process::WNOHANG).nil?]
|
|
38
36
|
end
|
|
39
37
|
|
|
40
|
-
# Stops the process
|
|
38
|
+
# Stops the process if it is running.
|
|
41
39
|
#
|
|
42
40
|
# The process is signalled using the configured signal (defaults to `INT` when not set).
|
|
43
41
|
#
|
|
@@ -54,8 +52,6 @@ module Nonnative
|
|
|
54
52
|
end
|
|
55
53
|
|
|
56
54
|
[pid, stopped]
|
|
57
|
-
ensure
|
|
58
|
-
proxy.stop
|
|
59
55
|
end
|
|
60
56
|
|
|
61
57
|
# Returns a memoized memory reader for the spawned process.
|
data/lib/nonnative/proxy.rb
CHANGED
|
@@ -4,8 +4,7 @@ module Nonnative
|
|
|
4
4
|
# Base class for proxy implementations.
|
|
5
5
|
#
|
|
6
6
|
# A proxy is responsible for interposing behavior between a client and a target service.
|
|
7
|
-
#
|
|
8
|
-
# instance via {Nonnative::ProxyFactory} based on `service.proxy.kind`.
|
|
7
|
+
# Runtime services create a proxy instance via {Nonnative::ProxyFactory} based on `service.proxy.kind`.
|
|
9
8
|
#
|
|
10
9
|
# Concrete proxies typically implement these public methods:
|
|
11
10
|
# - `start`: begin proxying (bind/listen, start threads, etc)
|
|
@@ -17,7 +16,7 @@ module Nonnative
|
|
|
17
16
|
# @see Nonnative::NoProxy
|
|
18
17
|
# @see Nonnative::FaultInjectionProxy
|
|
19
18
|
class Proxy
|
|
20
|
-
# @param service [Nonnative::
|
|
19
|
+
# @param service [Nonnative::ConfigurationService] service configuration with an attached proxy configuration
|
|
21
20
|
def initialize(service)
|
|
22
21
|
@service = service
|
|
23
22
|
end
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Nonnative
|
|
4
|
-
# Factory for creating proxy instances for
|
|
4
|
+
# Factory for creating proxy instances for services.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# using {Nonnative.proxy}.
|
|
6
|
+
# A runtime service constructs a proxy via this factory. The proxy implementation is selected by
|
|
7
|
+
# `service.proxy.kind` and resolved using {Nonnative.proxy}.
|
|
9
8
|
#
|
|
10
9
|
# If the kind is unknown (or `"none"`), {Nonnative.proxy} returns {Nonnative::NoProxy}.
|
|
11
10
|
#
|
|
@@ -15,9 +14,9 @@ module Nonnative
|
|
|
15
14
|
# @see Nonnative::NoProxy
|
|
16
15
|
class ProxyFactory
|
|
17
16
|
class << self
|
|
18
|
-
# Creates a proxy instance for the given
|
|
17
|
+
# Creates a proxy instance for the given service configuration.
|
|
19
18
|
#
|
|
20
|
-
# @param service [Nonnative::
|
|
19
|
+
# @param service [Nonnative::ConfigurationService] service configuration with an attached proxy configuration
|
|
21
20
|
# @return [Nonnative::Proxy] proxy instance (may be a {Nonnative::NoProxy})
|
|
22
21
|
def create(service)
|
|
23
22
|
proxy = Nonnative.proxy(service.proxy.kind)
|
data/lib/nonnative/runner.rb
CHANGED
|
@@ -10,21 +10,13 @@ module Nonnative
|
|
|
10
10
|
# - {Nonnative::Server} for in-process Ruby servers (threads)
|
|
11
11
|
# - {Nonnative::Service} for proxy-only external dependencies
|
|
12
12
|
#
|
|
13
|
-
# Each runner has an associated proxy instance created via {Nonnative::ProxyFactory}.
|
|
14
|
-
#
|
|
15
13
|
# @see Nonnative::Process
|
|
16
14
|
# @see Nonnative::Server
|
|
17
15
|
# @see Nonnative::Service
|
|
18
16
|
class Runner
|
|
19
|
-
# Returns the proxy instance for this runner.
|
|
20
|
-
#
|
|
21
|
-
# @return [Nonnative::Proxy]
|
|
22
|
-
attr_reader :proxy
|
|
23
|
-
|
|
24
17
|
# @param service [Nonnative::ConfigurationRunner] runner configuration
|
|
25
18
|
def initialize(service)
|
|
26
19
|
@service = service
|
|
27
|
-
@proxy = Nonnative::ProxyFactory.create(service)
|
|
28
20
|
end
|
|
29
21
|
|
|
30
22
|
# Returns the configured runner name.
|
data/lib/nonnative/server.rb
CHANGED
|
@@ -4,7 +4,6 @@ module Nonnative
|
|
|
4
4
|
# Runtime runner that manages an in-process Ruby server.
|
|
5
5
|
#
|
|
6
6
|
# A server runner:
|
|
7
|
-
# - starts the configured proxy (if any),
|
|
8
7
|
# - starts a Ruby thread that runs {#perform_start},
|
|
9
8
|
# - waits briefly (via the runner `wait`), and
|
|
10
9
|
# - participates in readiness/shutdown via TCP port checks orchestrated by {Nonnative::Pool}.
|
|
@@ -25,7 +24,7 @@ module Nonnative
|
|
|
25
24
|
@timeout = Nonnative::Timeout.new(service.timeout)
|
|
26
25
|
end
|
|
27
26
|
|
|
28
|
-
# Starts the
|
|
27
|
+
# Starts the server thread if it is not already started.
|
|
29
28
|
#
|
|
30
29
|
# @return [Array<(Integer, TrueClass)>]
|
|
31
30
|
# a tuple of:
|
|
@@ -33,7 +32,6 @@ module Nonnative
|
|
|
33
32
|
# - `true` (thread creation itself is considered started; readiness is checked separately)
|
|
34
33
|
def start
|
|
35
34
|
unless thread
|
|
36
|
-
proxy.start
|
|
37
35
|
@thread = Thread.new { perform_start }
|
|
38
36
|
|
|
39
37
|
wait_start
|
|
@@ -46,14 +44,13 @@ module Nonnative
|
|
|
46
44
|
|
|
47
45
|
# Stops the server if it is running.
|
|
48
46
|
#
|
|
49
|
-
# Calls {#perform_stop}, terminates the server thread,
|
|
47
|
+
# Calls {#perform_stop}, terminates the server thread, and waits briefly.
|
|
50
48
|
#
|
|
51
49
|
# @return [Integer] the server identifier (`object_id`)
|
|
52
50
|
def stop
|
|
53
51
|
if thread
|
|
54
52
|
perform_stop
|
|
55
53
|
thread.terminate
|
|
56
|
-
proxy.stop
|
|
57
54
|
|
|
58
55
|
@thread = nil
|
|
59
56
|
wait_stop
|
data/lib/nonnative/service.rb
CHANGED
|
@@ -12,6 +12,18 @@ module Nonnative
|
|
|
12
12
|
# @see Nonnative::ConfigurationService
|
|
13
13
|
# @see Nonnative::Proxy
|
|
14
14
|
class Service < Runner
|
|
15
|
+
# Returns the proxy instance for this service.
|
|
16
|
+
#
|
|
17
|
+
# @return [Nonnative::Proxy]
|
|
18
|
+
attr_reader :proxy
|
|
19
|
+
|
|
20
|
+
# @param service [Nonnative::ConfigurationService] service configuration
|
|
21
|
+
def initialize(service)
|
|
22
|
+
super
|
|
23
|
+
|
|
24
|
+
@proxy = Nonnative::ProxyFactory.create(service)
|
|
25
|
+
end
|
|
26
|
+
|
|
15
27
|
# Starts the configured proxy (if any).
|
|
16
28
|
#
|
|
17
29
|
# @return [void]
|
data/lib/nonnative/version.rb
CHANGED