-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into test-data
- Loading branch information
Showing
20 changed files
with
410 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
TEMP_TEST_OUTPUT=/tmp/contract-test-service.log | ||
|
||
# port 8000 and 9000 is already used in the CI environment because we're | ||
# running a DynamoDB container and an SSE contract test | ||
PORT=10000 | ||
|
||
build-contract-tests: | ||
@cd contract-tests && pip install -r requirements.txt | ||
|
||
start-contract-test-service: | ||
@cd contract-tests && python service.py $(PORT) | ||
|
||
start-contract-test-service-bg: | ||
@echo "Test service output will be captured in $(TEMP_TEST_OUTPUT)" | ||
@make start-contract-test-service >$(TEMP_TEST_OUTPUT) 2>&1 & | ||
|
||
run-contract-tests: | ||
@curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v1.0.0/downloader/run.sh \ | ||
| VERSION=v1 PARAMS="-url http://localhost:$(PORT) -debug -stop-service-at-end $(TEST_HARNESS_PARAMS)" sh | ||
|
||
contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests | ||
|
||
.PHONY: build-contract-tests start-contract-test-service run-contract-tests contract-tests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# SDK contract test service | ||
|
||
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. | ||
|
||
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. | ||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import logging | ||
import os | ||
import sys | ||
|
||
# Import ldclient from parent directory | ||
sys.path.insert(1, os.path.join(sys.path[0], '..')) | ||
from ldclient import * | ||
|
||
def millis_to_seconds(t): | ||
return None if t is None else t / 1000 | ||
|
||
|
||
class ClientEntity: | ||
def __init__(self, tag, config): | ||
self.log = logging.getLogger(tag) | ||
opts = {"sdk_key": config["credential"]} | ||
|
||
if "streaming" in config: | ||
streaming = config["streaming"] | ||
if "baseUri" in streaming: | ||
opts["stream_uri"] = streaming["baseUri"] | ||
if streaming.get("initialRetryDelayMs") is not None: | ||
opts["initial_reconnect_delay"] = streaming["initialRetryDelayMs"] / 1000.0 | ||
|
||
if "events" in config: | ||
events = config["events"] | ||
if "baseUri" in events: | ||
opts["events_uri"] = events["baseUri"] | ||
if events.get("capacity", None) is not None: | ||
opts["events_max_pending"] = events["capacity"] | ||
opts["diagnostic_opt_out"] = not events.get("enableDiagnostics", False) | ||
opts["all_attributes_private"] = events.get("allAttributesPrivate", False) | ||
opts["private_attribute_names"] = events.get("globalPrivateAttributes", {}) | ||
if "flushIntervalMs" in events: | ||
opts["flush_interval"] = events["flushIntervalMs"] / 1000.0 | ||
if "inlineUsers" in events: | ||
opts["inline_users_in_events"] = events["inlineUsers"] | ||
else: | ||
opts["send_events"] = False | ||
|
||
start_wait = config.get("startWaitTimeMs", 5000) | ||
config = Config(**opts) | ||
|
||
self.client = client.LDClient(config, start_wait / 1000.0) | ||
|
||
def is_initializing(self) -> bool: | ||
return self.client.is_initialized() | ||
|
||
def evaluate(self, params) -> dict: | ||
response = {} | ||
|
||
if params.get("detail", False): | ||
detail = self.client.variation_detail(params["flagKey"], params["user"], params["defaultValue"]) | ||
response["value"] = detail.value | ||
response["variationIndex"] = detail.variation_index | ||
response["reason"] = detail.reason | ||
else: | ||
response["value"] = self.client.variation(params["flagKey"], params["user"], params["defaultValue"]) | ||
|
||
return response | ||
|
||
def evaluate_all(self, params): | ||
opts = {} | ||
opts["client_side_only"] = params.get("clientSideOnly", False) | ||
opts["with_reasons"] = params.get("withReasons", False) | ||
opts["details_only_for_tracked_flags"] = params.get("detailsOnlyForTrackedFlags", False) | ||
|
||
state = self.client.all_flags_state(params["user"], **opts) | ||
|
||
return {"state": state.to_json_dict()} | ||
|
||
def track(self, params): | ||
self.client.track(params["eventKey"], params["user"], params["data"], params.get("metricValue", None)) | ||
|
||
def identify(self, params): | ||
self.client.identify(params["user"]) | ||
|
||
def alias(self, params): | ||
self.client.alias(params["user"], params["previousUser"]) | ||
|
||
def flush(self): | ||
self.client.flush() | ||
|
||
def close(self): | ||
self.client.close() | ||
self.log.info('Test ended') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Flask==1.1.4 | ||
urllib3>=1.22.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
from client_entity import ClientEntity | ||
|
||
import json | ||
import logging | ||
import os | ||
import sys | ||
from flask import Flask, request, jsonify | ||
from flask.logging import default_handler | ||
from logging.config import dictConfig | ||
from werkzeug.exceptions import HTTPException | ||
|
||
|
||
default_port = 8000 | ||
|
||
# logging configuration | ||
dictConfig({ | ||
'version': 1, | ||
'formatters': { | ||
'default': { | ||
'format': '[%(asctime)s] [%(name)s] %(levelname)s: %(message)s', | ||
} | ||
}, | ||
'handlers': { | ||
'console': { | ||
'class': 'logging.StreamHandler', | ||
'formatter': 'default' | ||
} | ||
}, | ||
'root': { | ||
'level': 'INFO', | ||
'handlers': ['console'] | ||
}, | ||
'ldclient.util': { | ||
'level': 'INFO', | ||
'handlers': ['console'] | ||
}, | ||
'loggers': { | ||
'werkzeug': { 'level': 'ERROR' } # disable irrelevant Flask app logging | ||
} | ||
}) | ||
|
||
app = Flask(__name__) | ||
app.logger.removeHandler(default_handler) | ||
|
||
client_counter = 0 | ||
clients = {} | ||
global_log = logging.getLogger('testservice') | ||
|
||
|
||
@app.errorhandler(Exception) | ||
def handle_exception(e): | ||
# pass through HTTP errors | ||
if isinstance(e, HTTPException): | ||
return e | ||
|
||
return str(e), 500 | ||
|
||
@app.route('/', methods=['GET']) | ||
def status(): | ||
body = { | ||
'capabilities': [ | ||
'server-side', | ||
'all-flags-with-reasons', | ||
'all-flags-client-side-only', | ||
'all-flags-details-only-for-tracked-flags', | ||
] | ||
} | ||
return (json.dumps(body), 200, {'Content-type': 'application/json'}) | ||
|
||
@app.route('/', methods=['DELETE']) | ||
def delete_stop_service(): | ||
global_log.info("Test service has told us to exit") | ||
os._exit(0) | ||
|
||
@app.route('/', methods=['POST']) | ||
def post_create_client(): | ||
global client_counter, clients | ||
|
||
options = request.get_json() | ||
|
||
client_counter += 1 | ||
client_id = str(client_counter) | ||
resource_url = '/clients/%s' % client_id | ||
|
||
client = ClientEntity(options['tag'], options['configuration']) | ||
|
||
if client.is_initializing() is False and options['configuration'].get('initCanFail', False) is False: | ||
client.close() | ||
return ("Failed to initialize", 500) | ||
|
||
clients[client_id] = client | ||
return ('', 201, {'Location': resource_url}) | ||
|
||
|
||
@app.route('/clients/<id>', methods=['POST']) | ||
def post_client_command(id): | ||
global clients | ||
|
||
params = request.get_json() | ||
|
||
client = clients[id] | ||
if client is None: | ||
return ('', 404) | ||
|
||
if params.get('command') == "evaluate": | ||
response = client.evaluate(params.get("evaluate")) | ||
return (json.dumps(response), 200) | ||
elif params.get("command") == "evaluateAll": | ||
response = client.evaluate_all(params.get("evaluateAll")) | ||
return (json.dumps(response), 200) | ||
elif params.get("command") == "customEvent": | ||
client.track(params.get("customEvent")) | ||
return ('', 201) | ||
elif params.get("command") == "identifyEvent": | ||
client.identify(params.get("identifyEvent")) | ||
return ('', 201) | ||
elif params.get("command") == "aliasEvent": | ||
client.alias(params.get("aliasEvent")) | ||
return ('', 201) | ||
elif params.get('command') == "flushEvents": | ||
client.flush() | ||
return ('', 201) | ||
|
||
return ('', 400) | ||
|
||
@app.route('/clients/<id>', methods=['DELETE']) | ||
def delete_client(id): | ||
global clients | ||
|
||
client = clients[id] | ||
if client is None: | ||
return ('', 404) | ||
|
||
client.close() | ||
return ('', 204) | ||
|
||
if __name__ == "__main__": | ||
port = default_port | ||
if sys.argv[len(sys.argv) - 1] != 'service.py': | ||
port = int(sys.argv[len(sys.argv) - 1]) | ||
global_log.info('Listening on port %d', port) | ||
app.run(host='0.0.0.0', port=port) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.