Skip to content

Commit 5af6e9a

Browse files
authored
Add contract tests (#178)
1 parent 32e74ed commit 5af6e9a

File tree

7 files changed

+259
-8
lines changed

7 files changed

+259
-8
lines changed

.circleci/config.yml

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ workflows:
1717
name: Ruby 3.0
1818
docker-image: cimg/ruby:3.0
1919
- build-test-linux:
20-
name: JRuby 9.2
21-
docker-image: jruby:9.2-jdk
20+
name: JRuby 9.3
21+
docker-image: jruby:9.3-jdk
2222
jruby: true
2323

2424
jobs:
@@ -51,9 +51,20 @@ jobs:
5151
- run: ruby -v
5252
- run: gem install bundler -v 2.2.33
5353
- run: bundle _2.2.33_ install
54-
- run: mkdir ./rspec
55-
- run: bundle _2.2.33_ exec rspec --format documentation --format RspecJunitFormatter -o ./rspec/rspec.xml spec
54+
- run: mkdir /tmp/circle-artifacts
55+
- run: bundle _2.2.33_ exec rspec --format documentation --format RspecJunitFormatter -o /tmp/circle-artifacts/rspec.xml spec
56+
57+
- when:
58+
condition:
59+
not: <<parameters.jruby>>
60+
steps:
61+
- run: make build-contract-tests
62+
- run:
63+
command: make start-contract-test-service
64+
background: true
65+
- run: TEST_HARNESS_PARAMS="-junit /tmp/circle-artifacts/contract-tests-junit.xml" make run-contract-tests
66+
5667
- store_test_results:
57-
path: ./rspec
68+
path: /tmp/circle-artifacts
5869
- store_artifacts:
59-
path: ./rspec
70+
path: /tmp/circle-artifacts

Makefile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
TEMP_TEST_OUTPUT=/tmp/contract-test-service.log
2+
3+
build-contract-tests:
4+
@cd contract-tests && bundle _2.2.33_ install
5+
6+
start-contract-test-service:
7+
@cd contract-tests && bundle _2.2.33_ exec ruby service.rb
8+
9+
start-contract-test-service-bg:
10+
@echo "Test service output will be captured in $(TEMP_TEST_OUTPUT)"
11+
@make start-contract-test-service >$(TEMP_TEST_OUTPUT) 2>&1 &
12+
13+
run-contract-tests:
14+
@curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v1.0.0/downloader/run.sh \
15+
| VERSION=v1 PARAMS="-url http://localhost:9000 -debug -stop-service-at-end $(TEST_HARNESS_PARAMS)" sh
16+
17+
contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests
18+
19+
.PHONY: build-contract-tests start-contract-test-service run-contract-tests contract-tests

contract-tests/Gemfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
source 'https://rubygems.org'
2+
3+
gem 'launchdarkly-server-sdk', path: '..'
4+
5+
gem 'sinatra', '~> 2.1'
6+
# Sinatra can work with several server frameworks. In JRuby, we have to use glassfish (which
7+
# is only available in JRuby). Otherwise we use thin (which is not available in JRuby).
8+
gem 'glassfish', :platforms => :jruby
9+
gem 'thin', :platforms => :ruby
10+
gem 'json'

contract-tests/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# SDK contract test service
2+
3+
This directory contains an implementation of the cross-platform SDK testing protocol defined by https://github.com/launchdarkly/sdk-test-harness. See that project's `README` for details of this protocol, and the kinds of SDK capabilities that are relevant to the contract tests. This code should not need to be updated unless the SDK has added or removed such capabilities.
4+
5+
To run these tests locally, run `make contract-tests` from the SDK project root directory. This downloads the correct version of the test harness tool automatically.
6+
7+
Or, to test against an in-progress local version of the test harness, run `make start-contract-test-service` from the SDK project root directory; then, in the root directory of the `sdk-test-harness` project, build the test harness and run it from the command line.

contract-tests/client_entity.rb

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
require 'ld-eventsource'
2+
require 'json'
3+
require 'net/http'
4+
5+
class ClientEntity
6+
def initialize(log, config)
7+
@log = log
8+
9+
opts = {}
10+
11+
opts[:logger] = log
12+
13+
if config[:streaming]
14+
streaming = config[:streaming]
15+
opts[:stream_uri] = streaming[:baseUri] if !streaming[:baseUri].nil?
16+
opts[:initial_reconnect_delay] = streaming[:initialRetryDelayMs] / 1_000.0 if !streaming[:initialRetryDelayMs].nil?
17+
end
18+
19+
if config[:events]
20+
events = config[:events]
21+
opts[:events_uri] = events[:baseUri] if events[:baseUri]
22+
opts[:capacity] = events[:capacity] if events[:capacity]
23+
opts[:diagnostic_opt_out] = !events[:enableDiagnostics]
24+
opts[:all_attributes_private] = !!events[:allAttributesPrivate]
25+
opts[:private_attribute_names] = events[:globalPrivateAttributes]
26+
opts[:flush_interval] = (events[:flushIntervalMs] / 1_000) if events.has_key? :flushIntervalMs
27+
opts[:inline_users_in_events] = events[:inlineUsers] || false
28+
else
29+
opts[:send_events] = false
30+
end
31+
32+
startWaitTimeMs = config[:startWaitTimeMs] || 5_000
33+
34+
@client = LaunchDarkly::LDClient.new(
35+
config[:credential],
36+
LaunchDarkly::Config.new(opts),
37+
startWaitTimeMs / 1_000.0)
38+
end
39+
40+
def initialized?
41+
@client.initialized?
42+
end
43+
44+
def evaluate(params)
45+
response = {}
46+
47+
if params[:detail]
48+
detail = @client.variation_detail(params[:flagKey], params[:user], params[:defaultValue])
49+
response[:value] = detail.value
50+
response[:variationIndex] = detail.variation_index
51+
response[:reason] = detail.reason
52+
else
53+
response[:value] = @client.variation(params[:flagKey], params[:user], params[:defaultValue])
54+
end
55+
56+
response
57+
end
58+
59+
def evaluate_all(params)
60+
opts = {}
61+
opts[:client_side_only] = params[:clientSideOnly] || false
62+
opts[:with_reasons] = params[:withReasons] || false
63+
opts[:details_only_for_tracked_flags] = params[:detailsOnlyForTrackedFlags] || false
64+
65+
@client.all_flags_state(params[:user], opts)
66+
end
67+
68+
def track(params)
69+
@client.track(params[:eventKey], params[:user], params[:data], params[:metricValue])
70+
end
71+
72+
def identify(params)
73+
@client.identify(params[:user])
74+
end
75+
76+
def alias(params)
77+
@client.alias(params[:user], params[:previousUser])
78+
end
79+
80+
def flush_events
81+
@client.flush
82+
end
83+
84+
def log
85+
@log
86+
end
87+
88+
def close
89+
@client.close
90+
@log.info("Test ended")
91+
end
92+
end

contract-tests/service.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
require 'launchdarkly-server-sdk'
2+
require 'json'
3+
require 'logger'
4+
require 'net/http'
5+
require 'sinatra'
6+
7+
require './client_entity.rb'
8+
9+
configure :development do
10+
disable :show_exceptions
11+
end
12+
13+
$log = Logger.new(STDOUT)
14+
$log.formatter = proc {|severity, datetime, progname, msg|
15+
"[GLOBAL] #{datetime.strftime('%Y-%m-%d %H:%M:%S.%3N')} #{severity} #{progname} #{msg}\n"
16+
}
17+
18+
set :port, 9000
19+
set :logging, false
20+
21+
clients = {}
22+
clientCounter = 0
23+
24+
get '/' do
25+
{
26+
capabilities: [
27+
'server-side',
28+
'all-flags-with-reasons',
29+
'all-flags-client-side-only',
30+
'all-flags-details-only-for-tracked-flags',
31+
]
32+
}.to_json
33+
end
34+
35+
delete '/' do
36+
$log.info("Test service has told us to exit")
37+
Thread.new { sleep 1; exit }
38+
return 204
39+
end
40+
41+
post '/' do
42+
opts = JSON.parse(request.body.read, :symbolize_names => true)
43+
tag = "[#{opts[:tag]}]"
44+
45+
clientCounter += 1
46+
clientId = clientCounter.to_s
47+
48+
log = Logger.new(STDOUT)
49+
log.formatter = proc {|severity, datetime, progname, msg|
50+
"#{tag} #{datetime.strftime('%Y-%m-%d %H:%M:%S.%3N')} #{severity} #{progname} #{msg}\n"
51+
}
52+
53+
log.info("Starting client")
54+
log.debug("Parameters: #{opts}")
55+
56+
client = ClientEntity.new(log, opts[:configuration])
57+
58+
if !client.initialized? && opts[:configuration][:initCanFail] == false
59+
client.close()
60+
return [500, nil, "Failed to initialize"]
61+
end
62+
63+
clientResourceUrl = "/clients/#{clientId}"
64+
clients[clientId] = client
65+
return [201, {'Location' => clientResourceUrl}, nil]
66+
end
67+
68+
post '/clients/:id' do |clientId|
69+
client = clients[clientId]
70+
return 404 if client.nil?
71+
72+
params = JSON.parse(request.body.read, :symbolize_names => true)
73+
74+
client.log.info("Processing request for client #{clientId}")
75+
client.log.debug("Parameters: #{params}")
76+
77+
case params[:command]
78+
when "evaluate"
79+
response = client.evaluate(params[:evaluate])
80+
return [200, nil, response.to_json]
81+
when "evaluateAll"
82+
response = {:state => client.evaluate_all(params[:evaluateAll])}
83+
return [200, nil, response.to_json]
84+
when "customEvent"
85+
client.track(params[:customEvent])
86+
return 201
87+
when "identifyEvent"
88+
client.identify(params[:identifyEvent])
89+
return 201
90+
when "aliasEvent"
91+
client.alias(params[:aliasEvent])
92+
return 201
93+
when "flushEvents"
94+
client.flush_events
95+
return 201
96+
end
97+
98+
return [400, nil, {:error => "Unknown command requested"}.to_json]
99+
end
100+
101+
delete '/clients/:id' do |clientId|
102+
client = clients[clientId]
103+
return 404 if client.nil?
104+
clients.delete(clientId)
105+
client.close
106+
107+
return 204
108+
end
109+
110+
error do
111+
env['sinatra.error'].message
112+
end

lib/ldclient-rb/ldclient.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def initialize(sdk_key, config = Config.default, wait_for_sec = 5)
6565
get_segment = lambda { |key| @store.get(SEGMENTS, key) }
6666
get_big_segments_membership = lambda { |key| @big_segment_store_manager.get_user_membership(key) }
6767
@evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, get_big_segments_membership, @config.logger)
68-
68+
6969
if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out?
7070
diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key))
7171
else
@@ -178,7 +178,7 @@ def initialized?
178178
# Other supported user attributes include IP address, country code, and an arbitrary hash of
179179
# custom attributes. For more about the supported user properties and how they work in
180180
# LaunchDarkly, see [Targeting users](https://docs.launchdarkly.com/home/flags/targeting-users).
181-
#
181+
#
182182
# The optional `:privateAttributeNames` user property allows you to specify a list of
183183
# attribute names that should not be sent back to LaunchDarkly.
184184
# [Private attributes](https://docs.launchdarkly.com/home/users/attributes#creating-private-user-attributes)

0 commit comments

Comments
 (0)