quonfig 0.0.6 → 0.0.9
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/CHANGELOG.md +29 -0
- data/VERSION +1 -1
- data/lib/quonfig/bound_client.rb +26 -0
- data/lib/quonfig/client.rb +212 -3
- data/lib/quonfig/context.rb +10 -1
- data/lib/quonfig/datadir.rb +2 -4
- data/lib/quonfig/dev_context.rb +41 -0
- data/lib/quonfig/errors/decryption_error.rb +20 -0
- data/lib/quonfig/errors/env_var_parse_error.rb +8 -1
- data/lib/quonfig/errors/invalid_environment_error.rb +19 -0
- data/lib/quonfig/errors/missing_environment_error.rb +18 -0
- data/lib/quonfig/evaluator.rb +84 -3
- data/lib/quonfig/http_connection.rb +1 -1
- data/lib/quonfig/options.rb +4 -1
- data/lib/quonfig/resolver.rb +215 -2
- data/lib/quonfig/stdlib_formatter.rb +95 -0
- data/lib/quonfig/telemetry/context_shape.rb +33 -0
- data/lib/quonfig/telemetry/context_shape_aggregator.rb +82 -0
- data/lib/quonfig/telemetry/evaluation_summaries_aggregator.rb +119 -0
- data/lib/quonfig/telemetry/example_contexts_aggregator.rb +101 -0
- data/lib/quonfig/telemetry/telemetry_reporter.rb +212 -0
- data/lib/quonfig.rb +10 -0
- data/quonfig.gemspec +23 -4
- data/test/integration/test_context_precedence.rb +35 -117
- data/test/integration/test_datadir_environment.rb +15 -37
- data/test/integration/test_dev_overrides.rb +40 -0
- data/test/integration/test_enabled.rb +157 -463
- data/test/integration/test_enabled_with_contexts.rb +19 -49
- data/test/integration/test_get.rb +43 -131
- data/test/integration/test_get_feature_flag.rb +7 -13
- data/test/integration/test_get_or_raise.rb +19 -45
- data/test/integration/test_get_weighted_values.rb +9 -4
- data/test/integration/test_helpers.rb +532 -4
- data/test/integration/test_post.rb +15 -5
- data/test/integration/test_telemetry.rb +77 -21
- data/test/test_client_telemetry.rb +175 -0
- data/test/test_context.rb +4 -1
- data/test/test_context_shape.rb +37 -0
- data/test/test_context_shape_aggregator.rb +126 -0
- data/test/test_datadir.rb +6 -2
- data/test/test_dev_context.rb +163 -0
- data/test/test_evaluation_summaries_aggregator.rb +180 -0
- data/test/test_example_contexts_aggregator.rb +119 -0
- data/test/test_http_connection.rb +1 -1
- data/test/test_resolver.rb +149 -2
- data/test/test_should_log.rb +186 -0
- data/test/test_stdlib_formatter.rb +195 -0
- data/test/test_telemetry_reporter.rb +209 -0
- metadata +22 -3
- data/scripts/generate_integration_tests.rb +0 -362
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quonfig
|
|
4
|
+
module Telemetry
|
|
5
|
+
# Owns the background thread that periodically drains the context
|
|
6
|
+
# aggregators and POSTs a JSON telemetry batch to
|
|
7
|
+
# +<telemetry_destination>/api/v1/telemetry/+.
|
|
8
|
+
#
|
|
9
|
+
# Wire shape matches api-telemetry's TelemetryEventsSchema:
|
|
10
|
+
#
|
|
11
|
+
# {
|
|
12
|
+
# "instanceHash": "...",
|
|
13
|
+
# "events": [
|
|
14
|
+
# { "summaries": { "start": ..., "end": ..., "summaries": [...] } },
|
|
15
|
+
# { "contextShapes": { "shapes": [...] } },
|
|
16
|
+
# { "exampleContexts": { "examples": [...] } }
|
|
17
|
+
# ]
|
|
18
|
+
# }
|
|
19
|
+
#
|
|
20
|
+
# Auth is HTTP Basic with username "1" and the SDK key as password
|
|
21
|
+
# (matching sdk-node and sdk-go). The +X-Quonfig-SDK-Version+ header
|
|
22
|
+
# carries the +ruby-<VERSION>+ identifier.
|
|
23
|
+
class TelemetryReporter
|
|
24
|
+
LOG = Quonfig::InternalLogger.new(self)
|
|
25
|
+
|
|
26
|
+
DEFAULT_INITIAL_DELAY_SECONDS = 8
|
|
27
|
+
DEFAULT_MAX_DELAY_SECONDS = 600
|
|
28
|
+
|
|
29
|
+
def initialize(options:, instance_hash:,
|
|
30
|
+
context_shape_aggregator: nil,
|
|
31
|
+
example_contexts_aggregator: nil,
|
|
32
|
+
evaluation_summaries_aggregator: nil,
|
|
33
|
+
sync_interval: nil,
|
|
34
|
+
http_connection: nil)
|
|
35
|
+
@options = options
|
|
36
|
+
@instance_hash = instance_hash
|
|
37
|
+
@sdk_key = options.sdk_key
|
|
38
|
+
@telemetry_destination = options.telemetry_destination
|
|
39
|
+
@context_shape_aggregator = context_shape_aggregator
|
|
40
|
+
@example_contexts_aggregator = example_contexts_aggregator
|
|
41
|
+
@evaluation_summaries_aggregator = evaluation_summaries_aggregator
|
|
42
|
+
@http_connection = http_connection
|
|
43
|
+
@sync_interval = calculate_sync_interval(sync_interval)
|
|
44
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
|
45
|
+
@thread = nil
|
|
46
|
+
@at_exit_registered = false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def enabled?
|
|
50
|
+
return false if @sdk_key.nil? || @sdk_key.to_s.empty?
|
|
51
|
+
return false if @telemetry_destination.nil? || @telemetry_destination.to_s.empty?
|
|
52
|
+
|
|
53
|
+
!@context_shape_aggregator.nil? ||
|
|
54
|
+
!@example_contexts_aggregator.nil? ||
|
|
55
|
+
!@evaluation_summaries_aggregator.nil?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Record a context across the context-driven aggregators. Evaluation
|
|
59
|
+
# summaries are recorded separately via
|
|
60
|
+
# +record_evaluation(...)+ since they require the evaluation result.
|
|
61
|
+
def record(context)
|
|
62
|
+
return if context.nil?
|
|
63
|
+
|
|
64
|
+
@context_shape_aggregator&.push(context)
|
|
65
|
+
@example_contexts_aggregator&.record(context)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def record_evaluation(**kwargs)
|
|
69
|
+
@evaluation_summaries_aggregator&.record(**kwargs)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def start
|
|
73
|
+
return if @thread&.alive?
|
|
74
|
+
return unless enabled?
|
|
75
|
+
|
|
76
|
+
@stopped.make_false
|
|
77
|
+
register_at_exit_handler
|
|
78
|
+
@thread = Thread.new do
|
|
79
|
+
Thread.current.name = 'quonfig-telemetry-reporter'
|
|
80
|
+
LOG.debug "Telemetry reporter started instance_hash=#{@instance_hash} destination=#{@telemetry_destination}"
|
|
81
|
+
|
|
82
|
+
until @stopped.true?
|
|
83
|
+
begin
|
|
84
|
+
sleep_duration = @sync_interval.call
|
|
85
|
+
slept = 0.0
|
|
86
|
+
step = 0.5
|
|
87
|
+
while slept < sleep_duration && !@stopped.true?
|
|
88
|
+
sleep([step, sleep_duration - slept].min)
|
|
89
|
+
slept += step
|
|
90
|
+
end
|
|
91
|
+
break if @stopped.true?
|
|
92
|
+
|
|
93
|
+
sync
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
LOG.warn "[quonfig] Telemetry reporter error: #{e.class}: #{e.message}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def stop
|
|
102
|
+
@stopped.make_true
|
|
103
|
+
thread = @thread
|
|
104
|
+
@thread = nil
|
|
105
|
+
thread&.wakeup if thread&.alive?
|
|
106
|
+
# Final drain attempt on stop so tests / short-lived processes
|
|
107
|
+
# don't silently drop pending telemetry.
|
|
108
|
+
begin
|
|
109
|
+
sync
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
LOG.debug "[quonfig] Final telemetry sync failed: #{e.class}: #{e.message}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Drain all aggregators and POST the batch. Public so tests can
|
|
116
|
+
# trigger a sync without waiting for the background loop.
|
|
117
|
+
def sync
|
|
118
|
+
events = []
|
|
119
|
+
if (summaries_event = @evaluation_summaries_aggregator&.drain_event)
|
|
120
|
+
events << summaries_event
|
|
121
|
+
end
|
|
122
|
+
if (shape_event = @context_shape_aggregator&.drain_event)
|
|
123
|
+
events << shape_event
|
|
124
|
+
end
|
|
125
|
+
if (example_event = @example_contexts_aggregator&.drain_event)
|
|
126
|
+
events << example_event
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
return if events.empty?
|
|
130
|
+
|
|
131
|
+
payload = {
|
|
132
|
+
'instanceHash' => @instance_hash,
|
|
133
|
+
'events' => events
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
post(payload)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Visible for tests.
|
|
140
|
+
def at_exit_registered?
|
|
141
|
+
@at_exit_registered
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# Rails / Passenger / Puma workers often terminate via SIGTERM without
|
|
147
|
+
# a chance to call Client#stop. Register a Kernel.at_exit hook on
|
|
148
|
+
# first start so the in-flight batch still gets flushed.
|
|
149
|
+
def register_at_exit_handler
|
|
150
|
+
return if @at_exit_registered
|
|
151
|
+
|
|
152
|
+
Kernel.at_exit { final_drain_on_exit }
|
|
153
|
+
@at_exit_registered = true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Wait this long for the background reporter thread to exit before
|
|
157
|
+
# giving up. Bounded so a thread blocked on a dead telemetry endpoint
|
|
158
|
+
# can't hang process exit.
|
|
159
|
+
AT_EXIT_THREAD_JOIN_TIMEOUT_SECONDS = 1.0
|
|
160
|
+
|
|
161
|
+
# Idempotent final drain. Safe to call after #stop has already
|
|
162
|
+
# drained: aggregators return nil when empty and #sync becomes a
|
|
163
|
+
# no-op. Bounded so a stuck reporter thread or dead telemetry
|
|
164
|
+
# endpoint can't hang process exit.
|
|
165
|
+
def final_drain_on_exit
|
|
166
|
+
@stopped.make_true
|
|
167
|
+
thread = @thread
|
|
168
|
+
@thread = nil
|
|
169
|
+
if thread&.alive?
|
|
170
|
+
thread.wakeup
|
|
171
|
+
thread.join(AT_EXIT_THREAD_JOIN_TIMEOUT_SECONDS)
|
|
172
|
+
end
|
|
173
|
+
sync
|
|
174
|
+
rescue StandardError => e
|
|
175
|
+
LOG.debug "[quonfig] at_exit telemetry drain failed: #{e.class}: #{e.message}"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def post(payload)
|
|
179
|
+
conn = http_connection
|
|
180
|
+
return if conn.nil?
|
|
181
|
+
|
|
182
|
+
response = conn.post('/api/v1/telemetry/', payload)
|
|
183
|
+
status = response.respond_to?(:status) ? response.status : nil
|
|
184
|
+
if status && status >= 400
|
|
185
|
+
LOG.warn "[quonfig] Telemetry POST failed: #{status}"
|
|
186
|
+
else
|
|
187
|
+
LOG.debug "[quonfig] Telemetry POST ok: events=#{payload['events'].size}"
|
|
188
|
+
end
|
|
189
|
+
response
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def http_connection
|
|
193
|
+
@http_connection ||= begin
|
|
194
|
+
return nil if @sdk_key.nil? || @telemetry_destination.nil?
|
|
195
|
+
|
|
196
|
+
Quonfig::HttpConnection.new(@telemetry_destination, @sdk_key)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def calculate_sync_interval(sync_interval)
|
|
201
|
+
return proc { sync_interval } if sync_interval.is_a?(Numeric)
|
|
202
|
+
return sync_interval if sync_interval.respond_to?(:call)
|
|
203
|
+
|
|
204
|
+
Quonfig::ExponentialBackoff.new(
|
|
205
|
+
initial_delay: DEFAULT_INITIAL_DELAY_SECONDS,
|
|
206
|
+
max_delay: DEFAULT_MAX_DELAY_SECONDS,
|
|
207
|
+
multiplier: 1.5
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
data/lib/quonfig.rb
CHANGED
|
@@ -25,6 +25,7 @@ require 'quonfig/error'
|
|
|
25
25
|
require 'quonfig/duration'
|
|
26
26
|
require 'quonfig/reason'
|
|
27
27
|
require 'quonfig/evaluation'
|
|
28
|
+
require 'quonfig/evaluation_details'
|
|
28
29
|
require 'quonfig/encryption'
|
|
29
30
|
require 'quonfig/exponential_backoff'
|
|
30
31
|
require 'quonfig/periodic_sync'
|
|
@@ -35,7 +36,11 @@ require 'quonfig/errors/env_var_parse_error'
|
|
|
35
36
|
require 'quonfig/errors/missing_env_var_error'
|
|
36
37
|
require 'quonfig/errors/type_mismatch_error'
|
|
37
38
|
require 'quonfig/errors/uninitialized_error'
|
|
39
|
+
require 'quonfig/errors/decryption_error'
|
|
40
|
+
require 'quonfig/errors/missing_environment_error'
|
|
41
|
+
require 'quonfig/errors/invalid_environment_error'
|
|
38
42
|
require 'quonfig/options'
|
|
43
|
+
require 'quonfig/dev_context'
|
|
39
44
|
require 'quonfig/rate_limit_cache'
|
|
40
45
|
require 'quonfig/weighted_value_resolver'
|
|
41
46
|
require 'quonfig/config_store'
|
|
@@ -48,6 +53,11 @@ require 'quonfig/sse_config_client'
|
|
|
48
53
|
require 'quonfig/http_connection'
|
|
49
54
|
require 'quonfig/caching_http_connection'
|
|
50
55
|
require 'quonfig/context'
|
|
56
|
+
require 'quonfig/telemetry/context_shape'
|
|
57
|
+
require 'quonfig/telemetry/context_shape_aggregator'
|
|
58
|
+
require 'quonfig/telemetry/example_contexts_aggregator'
|
|
59
|
+
require 'quonfig/telemetry/evaluation_summaries_aggregator'
|
|
60
|
+
require 'quonfig/telemetry/telemetry_reporter'
|
|
51
61
|
require 'quonfig/client'
|
|
52
62
|
require 'quonfig/bound_client'
|
|
53
63
|
require 'quonfig/semantic_logger_filter'
|
data/quonfig.gemspec
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
|
5
|
-
# stub: quonfig 0.0.
|
|
5
|
+
# stub: quonfig 0.0.9 ruby lib
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |s|
|
|
8
8
|
s.name = "quonfig".freeze
|
|
9
|
-
s.version =
|
|
9
|
+
s.version = "0.0.9".freeze
|
|
10
10
|
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
12
12
|
s.require_paths = ["lib".freeze]
|
|
13
13
|
s.authors = ["Jeff Dwyer".freeze]
|
|
14
|
-
s.date = "2026-04-
|
|
14
|
+
s.date = "2026-04-27"
|
|
15
15
|
s.description = "Quonfig \u2014 feature flags and live config, stored as files in git.".freeze
|
|
16
16
|
s.email = "jeff@quonfig.com".freeze
|
|
17
17
|
s.extra_rdoc_files = [
|
|
@@ -54,14 +54,18 @@ Gem::Specification.new do |s|
|
|
|
54
54
|
"lib/quonfig/config_store.rb",
|
|
55
55
|
"lib/quonfig/context.rb",
|
|
56
56
|
"lib/quonfig/datadir.rb",
|
|
57
|
+
"lib/quonfig/dev_context.rb",
|
|
57
58
|
"lib/quonfig/duration.rb",
|
|
58
59
|
"lib/quonfig/encryption.rb",
|
|
59
60
|
"lib/quonfig/error.rb",
|
|
61
|
+
"lib/quonfig/errors/decryption_error.rb",
|
|
60
62
|
"lib/quonfig/errors/env_var_parse_error.rb",
|
|
61
63
|
"lib/quonfig/errors/initialization_timeout_error.rb",
|
|
64
|
+
"lib/quonfig/errors/invalid_environment_error.rb",
|
|
62
65
|
"lib/quonfig/errors/invalid_sdk_key_error.rb",
|
|
63
66
|
"lib/quonfig/errors/missing_default_error.rb",
|
|
64
67
|
"lib/quonfig/errors/missing_env_var_error.rb",
|
|
68
|
+
"lib/quonfig/errors/missing_environment_error.rb",
|
|
65
69
|
"lib/quonfig/errors/type_mismatch_error.rb",
|
|
66
70
|
"lib/quonfig/errors/uninitialized_error.rb",
|
|
67
71
|
"lib/quonfig/evaluation.rb",
|
|
@@ -80,14 +84,20 @@ Gem::Specification.new do |s|
|
|
|
80
84
|
"lib/quonfig/semantic_logger_filter.rb",
|
|
81
85
|
"lib/quonfig/semver.rb",
|
|
82
86
|
"lib/quonfig/sse_config_client.rb",
|
|
87
|
+
"lib/quonfig/stdlib_formatter.rb",
|
|
88
|
+
"lib/quonfig/telemetry/context_shape.rb",
|
|
89
|
+
"lib/quonfig/telemetry/context_shape_aggregator.rb",
|
|
90
|
+
"lib/quonfig/telemetry/evaluation_summaries_aggregator.rb",
|
|
91
|
+
"lib/quonfig/telemetry/example_contexts_aggregator.rb",
|
|
92
|
+
"lib/quonfig/telemetry/telemetry_reporter.rb",
|
|
83
93
|
"lib/quonfig/time_helpers.rb",
|
|
84
94
|
"lib/quonfig/types.rb",
|
|
85
95
|
"lib/quonfig/weighted_value_resolver.rb",
|
|
86
96
|
"quonfig.gemspec",
|
|
87
|
-
"scripts/generate_integration_tests.rb",
|
|
88
97
|
"test/fixtures/datafile.json",
|
|
89
98
|
"test/integration/test_context_precedence.rb",
|
|
90
99
|
"test/integration/test_datadir_environment.rb",
|
|
100
|
+
"test/integration/test_dev_overrides.rb",
|
|
91
101
|
"test/integration/test_enabled.rb",
|
|
92
102
|
"test/integration/test_enabled_with_contexts.rb",
|
|
93
103
|
"test/integration/test_get.rb",
|
|
@@ -105,12 +115,18 @@ Gem::Specification.new do |s|
|
|
|
105
115
|
"test/test_caching_http_connection.rb",
|
|
106
116
|
"test/test_client.rb",
|
|
107
117
|
"test/test_client_network_mode.rb",
|
|
118
|
+
"test/test_client_telemetry.rb",
|
|
108
119
|
"test/test_config_loader.rb",
|
|
109
120
|
"test/test_context.rb",
|
|
121
|
+
"test/test_context_shape.rb",
|
|
122
|
+
"test/test_context_shape_aggregator.rb",
|
|
110
123
|
"test/test_datadir.rb",
|
|
124
|
+
"test/test_dev_context.rb",
|
|
111
125
|
"test/test_duration.rb",
|
|
112
126
|
"test/test_encryption.rb",
|
|
127
|
+
"test/test_evaluation_summaries_aggregator.rb",
|
|
113
128
|
"test/test_evaluator.rb",
|
|
129
|
+
"test/test_example_contexts_aggregator.rb",
|
|
114
130
|
"test/test_exponential_backoff.rb",
|
|
115
131
|
"test/test_fixed_size_hash.rb",
|
|
116
132
|
"test/test_helper.rb",
|
|
@@ -123,7 +139,10 @@ Gem::Specification.new do |s|
|
|
|
123
139
|
"test/test_resolver.rb",
|
|
124
140
|
"test/test_semantic_logger_filter.rb",
|
|
125
141
|
"test/test_semver.rb",
|
|
142
|
+
"test/test_should_log.rb",
|
|
126
143
|
"test/test_sse_config_client.rb",
|
|
144
|
+
"test/test_stdlib_formatter.rb",
|
|
145
|
+
"test/test_telemetry_reporter.rb",
|
|
127
146
|
"test/test_typed_getters.rb",
|
|
128
147
|
"test/test_types.rb",
|
|
129
148
|
"test/test_weighted_value_resolver.rb"
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
#
|
|
3
3
|
# AUTO-GENERATED from integration-test-data/tests/eval/context_precedence.yaml.
|
|
4
|
-
# Regenerate with
|
|
4
|
+
# Regenerate with:
|
|
5
|
+
# cd integration-test-data/generators && npm run generate -- --target=ruby
|
|
6
|
+
# Source: integration-test-data/generators/src/targets/ruby.ts
|
|
5
7
|
# Do NOT edit by hand — changes will be overwritten.
|
|
6
8
|
|
|
7
9
|
require 'test_helper'
|
|
@@ -14,181 +16,97 @@ class TestContextPrecedence < Minitest::Test
|
|
|
14
16
|
|
|
15
17
|
# returns the correct `flag` value using the global context (1)
|
|
16
18
|
def test_returns_the_correct_flag_value_using_the_global_context_1
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
IntegrationTestHelpers.assert_resolved(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "verified"}}, true)
|
|
20
|
-
rescue Exception => e
|
|
21
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
22
|
-
end
|
|
19
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
20
|
+
IntegrationTestHelpers.assert_enabled(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "verified"}}, true)
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
# returns the correct `flag` value using the global context (2)
|
|
26
24
|
def test_returns_the_correct_flag_value_using_the_global_context_2
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
IntegrationTestHelpers.assert_resolved(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "?"}}, false)
|
|
30
|
-
rescue Exception => e
|
|
31
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
32
|
-
end
|
|
25
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
26
|
+
IntegrationTestHelpers.assert_enabled(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "?"}}, false)
|
|
33
27
|
end
|
|
34
28
|
|
|
35
29
|
# returns the correct `flag` value when local context clobbers global context (1)
|
|
36
30
|
def test_returns_the_correct_flag_value_when_local_context_clobbers_global_context_1
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
IntegrationTestHelpers.assert_resolved(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "verified"}}, true)
|
|
40
|
-
rescue Exception => e
|
|
41
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
42
|
-
end
|
|
31
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
32
|
+
IntegrationTestHelpers.assert_enabled(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "verified"}}, true)
|
|
43
33
|
end
|
|
44
34
|
|
|
45
35
|
# returns the correct `flag` value when local context clobbers global context (2)
|
|
46
36
|
def test_returns_the_correct_flag_value_when_local_context_clobbers_global_context_2
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
IntegrationTestHelpers.assert_resolved(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "?"}}, false)
|
|
50
|
-
rescue Exception => e
|
|
51
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
52
|
-
end
|
|
37
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
38
|
+
IntegrationTestHelpers.assert_enabled(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "?"}}, false)
|
|
53
39
|
end
|
|
54
40
|
|
|
55
41
|
# returns the correct `flag` value when block context clobbers global context (1)
|
|
56
42
|
def test_returns_the_correct_flag_value_when_block_context_clobbers_global_context_1
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
IntegrationTestHelpers.assert_resolved(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "?"}}, false)
|
|
60
|
-
rescue Exception => e
|
|
61
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
62
|
-
end
|
|
43
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
44
|
+
IntegrationTestHelpers.assert_enabled(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "?"}}, false)
|
|
63
45
|
end
|
|
64
46
|
|
|
65
47
|
# returns the correct `flag` value when block context clobbers global context (2)
|
|
66
48
|
def test_returns_the_correct_flag_value_when_block_context_clobbers_global_context_2
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
IntegrationTestHelpers.assert_resolved(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "verified"}}, true)
|
|
70
|
-
rescue Exception => e
|
|
71
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
72
|
-
end
|
|
49
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
50
|
+
IntegrationTestHelpers.assert_enabled(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "verified"}}, true)
|
|
73
51
|
end
|
|
74
52
|
|
|
75
53
|
# returns the correct `flag` value when local context clobbers block context (1)
|
|
76
54
|
def test_returns_the_correct_flag_value_when_local_context_clobbers_block_context_1
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
IntegrationTestHelpers.assert_resolved(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "?"}}, false)
|
|
80
|
-
rescue Exception => e
|
|
81
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
82
|
-
end
|
|
55
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
56
|
+
IntegrationTestHelpers.assert_enabled(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "?"}}, false)
|
|
83
57
|
end
|
|
84
58
|
|
|
85
59
|
# returns the correct `flag` value when local context clobbers block context (2)
|
|
86
60
|
def test_returns_the_correct_flag_value_when_local_context_clobbers_block_context_2
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
IntegrationTestHelpers.assert_resolved(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "verified"}}, true)
|
|
90
|
-
rescue Exception => e
|
|
91
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
92
|
-
end
|
|
61
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
62
|
+
IntegrationTestHelpers.assert_enabled(resolver, "mixed.case.property.name", {"user" => {"isHuman" => "verified"}}, true)
|
|
93
63
|
end
|
|
94
64
|
|
|
95
65
|
# returns the correct `get` value using the global context (1)
|
|
96
66
|
def test_returns_the_correct_get_value_using_the_global_context_1
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@prefab.cloud"}}, "override")
|
|
100
|
-
rescue Exception => e
|
|
101
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
102
|
-
end
|
|
67
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
68
|
+
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@prefab.cloud"}}, "override")
|
|
103
69
|
end
|
|
104
70
|
|
|
105
71
|
# returns the correct `get` value using the global context (2)
|
|
106
72
|
def test_returns_the_correct_get_value_using_the_global_context_2
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@example.com"}}, "default")
|
|
110
|
-
rescue Exception => e
|
|
111
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# returns the correct `get` value using the global context and api context (1)
|
|
116
|
-
def test_returns_the_correct_get_value_using_the_global_context_and_api_context_1
|
|
117
|
-
begin
|
|
118
|
-
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
119
|
-
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config.with.api.conditional", {"user" => {"email" => "test@prefab.cloud"}}, "override")
|
|
120
|
-
rescue Exception => e
|
|
121
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# returns the correct `get` value using the global context and api context (2)
|
|
126
|
-
def test_returns_the_correct_get_value_using_the_global_context_and_api_context_2
|
|
127
|
-
begin
|
|
128
|
-
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
129
|
-
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config.with.api.conditional", {"user" => {"email" => "test@example.com"}}, "api-override")
|
|
130
|
-
rescue Exception => e
|
|
131
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
132
|
-
end
|
|
73
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
74
|
+
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@example.com"}}, "default")
|
|
133
75
|
end
|
|
134
76
|
|
|
135
77
|
# returns the correct `get` value when local context clobbers global context (1)
|
|
136
78
|
def test_returns_the_correct_get_value_when_local_context_clobbers_global_context_1
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@prefab.cloud"}}, "override")
|
|
140
|
-
rescue Exception => e
|
|
141
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
142
|
-
end
|
|
79
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
80
|
+
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@prefab.cloud"}}, "override")
|
|
143
81
|
end
|
|
144
82
|
|
|
145
83
|
# returns the correct `get` value when local context clobbers global context (2)
|
|
146
84
|
def test_returns_the_correct_get_value_when_local_context_clobbers_global_context_2
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@example.com"}}, "default")
|
|
150
|
-
rescue Exception => e
|
|
151
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
152
|
-
end
|
|
85
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
86
|
+
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@example.com"}}, "default")
|
|
153
87
|
end
|
|
154
88
|
|
|
155
89
|
# returns the correct `get` value when block context clobbers global context (1)
|
|
156
90
|
def test_returns_the_correct_get_value_when_block_context_clobbers_global_context_1
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@example.com"}}, "default")
|
|
160
|
-
rescue Exception => e
|
|
161
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
162
|
-
end
|
|
91
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
92
|
+
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@example.com"}}, "default")
|
|
163
93
|
end
|
|
164
94
|
|
|
165
95
|
# returns the correct `get` value when block context clobbers global context (2)
|
|
166
96
|
def test_returns_the_correct_get_value_when_block_context_clobbers_global_context_2
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@prefab.cloud"}}, "override")
|
|
170
|
-
rescue Exception => e
|
|
171
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
172
|
-
end
|
|
97
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
98
|
+
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@prefab.cloud"}}, "override")
|
|
173
99
|
end
|
|
174
100
|
|
|
175
101
|
# returns the correct `get` value when local context clobbers block context (1)
|
|
176
102
|
def test_returns_the_correct_get_value_when_local_context_clobbers_block_context_1
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@example.com"}}, "default")
|
|
180
|
-
rescue Exception => e
|
|
181
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
182
|
-
end
|
|
103
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
104
|
+
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@example.com"}}, "default")
|
|
183
105
|
end
|
|
184
106
|
|
|
185
107
|
# returns the correct `get` value when local context clobbers block context (2)
|
|
186
108
|
def test_returns_the_correct_get_value_when_local_context_clobbers_block_context_2
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@prefab.cloud"}}, "override")
|
|
190
|
-
rescue Exception => e
|
|
191
|
-
skip("resolver not yet ported for this case: #{e.class}: #{e.message}")
|
|
192
|
-
end
|
|
109
|
+
resolver = IntegrationTestHelpers.build_resolver(@store)
|
|
110
|
+
IntegrationTestHelpers.assert_resolved(resolver, "basic.rule.config", {"user" => {"email" => "test@prefab.cloud"}}, "override")
|
|
193
111
|
end
|
|
194
112
|
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
#
|
|
3
3
|
# AUTO-GENERATED from integration-test-data/tests/eval/datadir_environment.yaml.
|
|
4
|
-
# Regenerate with
|
|
4
|
+
# Regenerate with:
|
|
5
|
+
# cd integration-test-data/generators && npm run generate -- --target=ruby
|
|
6
|
+
# Source: integration-test-data/generators/src/targets/ruby.ts
|
|
5
7
|
# Do NOT edit by hand — changes will be overwritten.
|
|
6
8
|
|
|
7
9
|
require 'test_helper'
|
|
@@ -14,63 +16,39 @@ class TestDatadirEnvironment < Minitest::Test
|
|
|
14
16
|
|
|
15
17
|
# datadir with environment option gets environment-specific value
|
|
16
18
|
def test_datadir_with_environment_option_gets_environment_specific_value
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
assert_equal "test4", client.get("james.test.key")
|
|
20
|
-
rescue Exception => e
|
|
21
|
-
skip("datadir Client.new not yet wired: #{e.class}: #{e.message}")
|
|
22
|
-
end
|
|
19
|
+
client = Quonfig::Client.new(datadir: IntegrationTestHelpers.data_dir, environment: "Production")
|
|
20
|
+
assert_equal "test4", client.get("james.test.key")
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
# datadir with QUONFIG_ENVIRONMENT env var gets environment-specific value
|
|
26
24
|
def test_datadir_with_quonfig_environment_env_var_gets_environment_specific_value
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
assert_equal "test4", client.get("james.test.key")
|
|
31
|
-
end
|
|
32
|
-
rescue Exception => e
|
|
33
|
-
skip("datadir Client.new not yet wired: #{e.class}: #{e.message}")
|
|
25
|
+
IntegrationTestHelpers.with_env({"QUONFIG_ENVIRONMENT" => "Production"}) do
|
|
26
|
+
client = Quonfig::Client.new(datadir: IntegrationTestHelpers.data_dir)
|
|
27
|
+
assert_equal "test4", client.get("james.test.key")
|
|
34
28
|
end
|
|
35
29
|
end
|
|
36
30
|
|
|
37
31
|
# environment option supersedes QUONFIG_ENVIRONMENT env var
|
|
38
32
|
def test_environment_option_supersedes_quonfig_environment_env_var
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
assert_equal "test4", client.get("james.test.key")
|
|
43
|
-
end
|
|
44
|
-
rescue Exception => e
|
|
45
|
-
skip("datadir Client.new not yet wired: #{e.class}: #{e.message}")
|
|
33
|
+
IntegrationTestHelpers.with_env({"QUONFIG_ENVIRONMENT" => "nonexistent"}) do
|
|
34
|
+
client = Quonfig::Client.new(datadir: IntegrationTestHelpers.data_dir, environment: "Production")
|
|
35
|
+
assert_equal "test4", client.get("james.test.key")
|
|
46
36
|
end
|
|
47
37
|
end
|
|
48
38
|
|
|
49
39
|
# config without environment override returns default value
|
|
50
40
|
def test_config_without_environment_override_returns_default_value
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
assert_equal "hello from no env row", client.get("config.with.only.default.env.row")
|
|
54
|
-
rescue Exception => e
|
|
55
|
-
skip("datadir Client.new not yet wired: #{e.class}: #{e.message}")
|
|
56
|
-
end
|
|
41
|
+
client = Quonfig::Client.new(datadir: IntegrationTestHelpers.data_dir, environment: "Production")
|
|
42
|
+
assert_equal "hello from no env row", client.get("config.with.only.default.env.row")
|
|
57
43
|
end
|
|
58
44
|
|
|
59
45
|
# datadir without environment fails to init
|
|
60
46
|
def test_datadir_without_environment_fails_to_init
|
|
61
|
-
|
|
62
|
-
skip("init raise-case (missing_environment) — no Quonfig::Errors mapping yet")
|
|
63
|
-
rescue Exception => e
|
|
64
|
-
skip("datadir Client.new not yet wired: #{e.class}: #{e.message}")
|
|
65
|
-
end
|
|
47
|
+
assert_raises(Quonfig::Errors::MissingEnvironmentError) { Quonfig::Client.new(datadir: IntegrationTestHelpers.data_dir) }
|
|
66
48
|
end
|
|
67
49
|
|
|
68
50
|
# datadir with invalid environment fails to init
|
|
69
51
|
def test_datadir_with_invalid_environment_fails_to_init
|
|
70
|
-
|
|
71
|
-
skip("init raise-case (invalid_environment) — no Quonfig::Errors mapping yet")
|
|
72
|
-
rescue Exception => e
|
|
73
|
-
skip("datadir Client.new not yet wired: #{e.class}: #{e.message}")
|
|
74
|
-
end
|
|
52
|
+
assert_raises(Quonfig::Errors::InvalidEnvironmentError) { Quonfig::Client.new(datadir: IntegrationTestHelpers.data_dir, environment: "nonexistent") }
|
|
75
53
|
end
|
|
76
54
|
end
|