From 2d31ac0012f474c9d17ba1d9fd328cd15e941f5b Mon Sep 17 00:00:00 2001 From: hroederld Date: Thu, 20 Feb 2020 15:13:08 -0800 Subject: [PATCH 01/25] [ch66649] initial copy from moonshot (and minor template fill in) --- CONTRIBUTING.md | 14 ++ README.md | 49 ++++++- launchdarkly.lua | 368 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTING.md create mode 100644 launchdarkly.lua diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cb41d17 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +Contributing to the LaunchDarkly Server SDK for Lua +================================================ + +LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. + +Submitting bug reports and feature requests +------------------ + +The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/lua-server-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. + +Submitting pull requests +------------------ + +We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. diff --git a/README.md b/README.md index fa37edb..fb7f090 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -# lua-server-sdk-private \ No newline at end of file +LaunchDarkly server SDK for Lua +=========================== + +[![Circle CI](https://circleci.com/gh/launchdarkly/lua-server-sdk.svg?style=shield)](https://circleci.com/gh/launchdarkly/lua-server-sdk) + +*This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* + +LaunchDarkly overview +------------------------- +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! + +[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) + +Getting started +----------- + +Refer to the [SDK documentation](https://docs.launchdarkly.com/docs/lua-server-reference#section-getting-started) for instructions on getting started with using the SDK. + +Learn more +----------- + +Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/docs/lua-server-reference). + +Testing +------- + +We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all behave correctly. + +Contributing +------------ + +We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK. + +About LaunchDarkly +----------- + +* LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: + * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. + * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). + * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. + * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list. +* Explore LaunchDarkly + * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information + * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides + * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation + * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates + * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies diff --git a/launchdarkly.lua b/launchdarkly.lua new file mode 100644 index 0000000..6468fec --- /dev/null +++ b/launchdarkly.lua @@ -0,0 +1,368 @@ +local ffi = require("ffi") +local cjson = require("cjson") + +local ld = {} + +ffi.cdef[[ + struct LDJSON; + typedef enum { + LDNull = 0, + LDText, + LDNumber, + LDBool, + LDObject, + LDArray + } LDJSONType; + struct LDJSON * LDNewNull(); + struct LDJSON * LDNewBool(const bool boolean); + struct LDJSON * LDNewNumber(const double number); + struct LDJSON * LDNewText(const char *const text); + struct LDJSON * LDNewObject(); + struct LDJSON * LDNewArray(); + bool LDSetNumber(struct LDJSON *const node, const double number); + void LDJSONFree(struct LDJSON *const json); + struct LDJSON * LDJSONDuplicate(const struct LDJSON *const json); + LDJSONType LDJSONGetType(const struct LDJSON *const json); + bool LDJSONCompare(const struct LDJSON *const left, + const struct LDJSON *const right); + bool LDGetBool(const struct LDJSON *const node); + double LDGetNumber(const struct LDJSON *const node); + const char * LDGetText(const struct LDJSON *const node); + struct LDJSON * LDIterNext(const struct LDJSON *const iter); + struct LDJSON * LDGetIter(const struct LDJSON *const collection); + const char * LDIterKey(const struct LDJSON *const iter); + unsigned int LDCollectionGetSize( + const struct LDJSON *const collection); + struct LDJSON * LDCollectionDetachIter( + struct LDJSON *const collection, struct LDJSON *const iter); + struct LDJSON * LDArrayLookup(const struct LDJSON *const array, + const unsigned int index); + bool LDArrayPush(struct LDJSON *const array, + struct LDJSON *const item); + bool LDArrayAppend(struct LDJSON *const prefix, + const struct LDJSON *const suffix); + struct LDJSON * LDObjectLookup(const struct LDJSON *const object, + const char *const key); + bool LDObjectSetKey(struct LDJSON *const object, + const char *const key, struct LDJSON *const item); + void LDObjectDeleteKey(struct LDJSON *const object, + const char *const key); + struct LDJSON * LDObjectDetachKey(struct LDJSON *const object, + const char *const key); + bool LDObjectMerge(struct LDJSON *const to, + const struct LDJSON *const from); + char * LDJSONSerialize(const struct LDJSON *const json); + struct LDJSON * LDJSONDeserialize(const char *const text); + struct LDStoreInterface; struct LDConfig; + struct LDConfig * LDConfigNew(const char *const key); + void LDConfigFree(struct LDConfig *const config); + bool LDConfigSetBaseURI(struct LDConfig *const config, + const char *const baseURI); + bool LDConfigSetStreamURI(struct LDConfig *const config, + const char *const streamURI); + bool LDConfigSetEventsURI(struct LDConfig *const config, + const char *const eventsURI); + void LDConfigSetStream(struct LDConfig *const config, + const bool stream); + void LDConfigSetSendEvents(struct LDConfig *const config, + const bool sendEvents); + void LDConfigSetEventsCapacity(struct LDConfig *const config, + const unsigned int eventsCapacity); + void LDConfigSetTimeout(struct LDConfig *const config, + const unsigned int milliseconds); + void LDConfigSetFlushInterval(struct LDConfig *const config, + const unsigned int milliseconds); + void LDConfigSetPollInterval(struct LDConfig *const config, + const unsigned int milliseconds); + void LDConfigSetOffline(struct LDConfig *const config, + const bool offline); + void LDConfigSetUseLDD(struct LDConfig *const config, + const bool useLDD); + void LDConfigSetAllAttributesPrivate(struct LDConfig *const config, + const bool allAttributesPrivate); + void LDConfigInlineUsersInEvents(struct LDConfig *const config, + const bool inlineUsersInEvents); + void LDConfigSetUserKeysCapacity(struct LDConfig *const config, + const unsigned int userKeysCapacity); + void LDConfigSetUserKeysFlushInterval(struct LDConfig *const config, + const unsigned int milliseconds); + bool LDConfigAddPrivateAttribute(struct LDConfig *const config, + const char *const attribute); + void LDConfigSetFeatureStoreBackend(struct LDConfig *const config, + struct LDStoreInterface *const backend); + struct LDUser * LDUserNew(const char *const userkey); + void LDUserFree(struct LDUser *const user); + void LDUserSetAnonymous(struct LDUser *const user, const bool anon); + bool LDUserSetIP(struct LDUser *const user, const char *const ip); + bool LDUserSetFirstName(struct LDUser *const user, + const char *const firstName); + bool LDUserSetLastName(struct LDUser *const user, + const char *const lastName); + bool LDUserSetEmail(struct LDUser *const user, + const char *const email); + bool LDUserSetName(struct LDUser *const user, + const char *const name); + bool LDUserSetAvatar(struct LDUser *const user, + const char *const avatar); + bool LDUserSetCountry(struct LDUser *const user, + const char *const country); + bool LDUserSetSecondary(struct LDUser *const user, + const char *const secondary); + void LDUserSetCustom(struct LDUser *const user, + struct LDJSON *const custom); + bool LDUserAddPrivateAttribute(struct LDUser *const user, + const char *const attribute); + struct LDClient * LDClientInit(struct LDConfig *const config, + const unsigned int maxwaitmilli); + void LDClientClose(struct LDClient *const client); + bool LDClientIsInitialized(struct LDClient *const client); + bool LDClientTrack(struct LDClient *const client, + const char *const key, const struct LDUser *const user, + struct LDJSON *const data); + bool LDClientTrackMetric(struct LDClient *const client, + const char *const key, const struct LDUser *const user, + struct LDJSON *const data, const double metric); + bool LDClientIdentify(struct LDClient *const client, + const struct LDUser *const user); + bool LDClientIsOffline(struct LDClient *const client); + void LDClientFlush(struct LDClient *const client); + void * LDAlloc(const size_t bytes); + void LDFree(void *const buffer); + char * LDStrDup(const char *const string); + void * LDRealloc(void *const buffer, const size_t bytes); + void * LDCalloc(const size_t nmemb, const size_t size); + char * LDStrNDup(const char *const str, const size_t n); + void LDSetMemoryRoutines(void *(*const newMalloc)(const size_t), + void (*const newFree)(void *const), + void *(*const newRealloc)(void *const, const size_t), + char *(*const newStrDup)(const char *const), + void *(*const newCalloc)(const size_t, const size_t), + char *(*const newStrNDup)(const char *const, const size_t)); + void LDGlobalInit(); + typedef enum { + LD_LOG_FATAL = 0, + LD_LOG_CRITICAL, + LD_LOG_ERROR, + LD_LOG_WARNING, + LD_LOG_INFO, + LD_LOG_DEBUG, + LD_LOG_TRACE + } LDLogLevel; + void LDi_log(const LDLogLevel level, const char *const format, ...); + void LDBasicLogger(const LDLogLevel level, const char *const text); + void LDConfigureGlobalLogger(const LDLogLevel level, + void (*logger)(const LDLogLevel level, const char *const text)); + const char * LDLogLevelToString(const LDLogLevel level); + enum LDEvalReason { + LD_UNKNOWN = 0, + LD_ERROR, + LD_OFF, + LD_PREREQUISITE_FAILED, + LD_TARGET_MATCH, + LD_RULE_MATCH, + LD_FALLTHROUGH + }; + enum LDEvalErrorKind { + LD_CLIENT_NOT_READY, + LD_NULL_KEY, + LD_STORE_ERROR, + LD_FLAG_NOT_FOUND, + LD_USER_NOT_SPECIFIED, + LD_MALFORMED_FLAG, + LD_WRONG_TYPE, + LD_OOM + }; + struct LDDetailsRule { + unsigned int ruleIndex; + char *id; + }; + struct LDDetails { + unsigned int variationIndex; + bool hasVariation; + enum LDEvalReason reason; + union { + enum LDEvalErrorKind errorKind; + char *prerequisiteKey; + struct LDDetailsRule rule; + } extra; + }; + void LDDetailsInit(struct LDDetails *const details); + void LDDetailsClear(struct LDDetails *const details); + const char * LDEvalReasonKindToString(const enum LDEvalReason kind); + const char * LDEvalErrorKindToString( + const enum LDEvalErrorKind kind); + struct LDJSON * LDReasonToJSON( + const struct LDDetails *const details); + bool LDBoolVariation(struct LDClient *const client, + struct LDUser *const user, const char *const key, const bool fallback, + struct LDDetails *const details); + int LDIntVariation(struct LDClient *const client, + struct LDUser *const user, const char *const key, const int fallback, + struct LDDetails *const details); + double LDDoubleVariation(struct LDClient *const client, + struct LDUser *const user, const char *const key, const double fallback, + struct LDDetails *const details); + char * LDStringVariation(struct LDClient *const client, + struct LDUser *const user, const char *const key, + const char* const fallback, struct LDDetails *const details); + struct LDJSON * LDJSONVariation(struct LDClient *const client, + struct LDUser *const user, const char *const key, + const struct LDJSON *const fallback, struct LDDetails *const details); + struct LDJSON * LDAllFlags(struct LDClient *const client, + struct LDUser *const user); +]] + +ld.so = ffi.load("ldserverapi") + +function applyWhenNotNil(subject, operation, value) + if value ~= nil and value ~= cjson.null then + operation(subject, value) + end +end + +function toLaunchDarklyJSON(x) + return ffi.gc(ld.so.LDJSONDeserialize(cjson.encode(x)), ld.so.LDJSONFree) +end + +function toLaunchDarklyJSONTransfer(x) + return ld.so.LDJSONDeserialize(cjson.encode(x)) +end + +function fromLaunchDarklyJSON(x) + local raw = ld.so.LDJSONSerialize(x) + local native = ffi.string(raw) + ld.so.LDFree(raw) + return cjson.decode(native) +end + +local makeConfig = function(fields) + local config = ld.so.LDConfigNew(fields["key"]) + + applyWhenNotNil(config, ld.so.LDConfigSetBaseURI, fields["baseURI"]) + applyWhenNotNil(config, ld.so.LDConfigSetStreamURI, fields["streamURI"]) + applyWhenNotNil(config, ld.so.LDConfigSetEventsURI, fields["eventsURI"]) + applyWhenNotNil(config, ld.so.LDConfigSetStream, fields["stream"]) + applyWhenNotNil(config, ld.so.LDConfigSetSendEvents, fields["sendEvents"]) + applyWhenNotNil(config, ld.so.LDConfigSetEventsCapacity, fields["eventsCapacity"]) + applyWhenNotNil(config, ld.so.LDConfigSetTimeout, fields["timeout"]) + applyWhenNotNil(config, ld.so.LDConfigSetFlushInterval, fields["flushInterval"]) + applyWhenNotNil(config, ld.so.LDConfigSetPollInterval, fields["pollInterval"]) + applyWhenNotNil(config, ld.so.LDConfigSetOffline, fields["offline"]) + applyWhenNotNil(config, ld.so.LDConfigSetAllAttributesPrivate, fields["allAttributesPrivate"]) + applyWhenNotNil(config, ld.so.LDConfigInlineUsersInEvents, fields["inlineUsersInEvents"]) + applyWhenNotNil(config, ld.so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) + applyWhenNotNil(config, ld.so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) + + local names = fields["privateAttributeNames"] + + if names ~= nil and names ~= cjson.null then + for _, v in ipairs(names) do + ld.so.LDConfigAddPrivateAttribute(config, v) + end + end + + return config +end + +ld.makeUser = function(fields) + local user = ffi.gc(ld.so.LDUserNew(fields["key"]), ld.so.LDUserFree) + + applyWhenNotNil(user, ld.so.LDUserSetAnonymous, fields["anonymous"]) + applyWhenNotNil(user, ld.so.LDUserSetIP, fields["ip"]) + applyWhenNotNil(user, ld.so.LDUserSetFirstName, fields["firstName"]) + applyWhenNotNil(user, ld.so.LDUserSetLastName, fields["lastName"]) + applyWhenNotNil(user, ld.so.LDUserSetEmail, fields["email"]) + applyWhenNotNil(user, ld.so.LDUserSetName, fields["name"]) + applyWhenNotNil(user, ld.so.LDUserSetAvatar, fields["avatar"]) + applyWhenNotNil(user, ld.so.LDUserSetCountry, fields["country"]) + applyWhenNotNil(user, ld.so.LDUserSetSecondary, fields["secondary"]) + + if fields["custom"] ~= nil then + ld.so.LDUserSetCustom(user, toLaunchDarklyJSONTransfer(fields["custom"])) + end + + local names = fields["privateAttributeNames"] + + if names ~= nil and names ~= cjson.null then + for _, v in ipairs(names) do + ngx.log(ngx.ERR, "value: " .. v) + ld.so.LDUserAddPrivateAttribute(user, v) + end + end + + return user +end + +ld.clientInit = function(config, timoutMilliseconds) + local interface = {} + + local client = ffi.gc(ld.so.LDClientInit(makeConfig(config), 1000), ld.so.LDClientClose) + + interface.isInitialized = function() + return ld.so.LDClientIsInitialized(client) + end + + interface.identify = function(user) + ld.so.LDClientIdentify(client, user) + end + + interface.isOffline = function() + ld.so.LDClientIsOffline(client) + end + + interface.flush = function() + ld.so.LDClientFlush(client) + end + + interface.track = function(key, user, data, metric) + local json = nil + + if data ~= nil then + json = toLaunchDarklyJSON(data) + end + + if metric ~= nil then + ld.so.LDClientTrackMetric(client, key, user, json, metric) + else + ld.so.LDClientTrack(client, key, user, json) + end + end + + interface.allFlags = function(user) + local x = ld.so.LDAllFlags(client, user) + if x ~= nil then + return fromLaunchDarklyJSON(x) + else + return nil + end + end + + interface.boolVariation = function(user, key, fallback) + return ld.so.LDBoolVariation(client, user, key, fallback, nil) + end + + interface.intVariation = function(user, key, fallback) + return ld.so.LDIntVariation(client, user, key, fallback, nil) + end + + interface.doubleVariation = function(user, key, fallback) + return ld.so.LDDoubleVariation(client, user, key, fallback, nil) + end + + interface.stringVariation = function(user, key, fallback) + local raw = ld.so.LDStringVariation(client, user, key, fallback, nil) + local native = ffi.string(raw) + ld.so.LDFree(raw) + return native + end + + interface.jsonVariation = function(user, key, fallback) + return fromLaunchDarklyJSON( + ld.so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) + ) + end + + return interface +end + +return ld From b41bd5a59314d6a337db02322048a096e3254870 Mon Sep 17 00:00:00 2001 From: hroederld Date: Thu, 27 Feb 2020 16:26:47 -0800 Subject: [PATCH 02/25] [ch66755] Add variationDetail variation handlers (#2) --- launchdarkly.lua | 64 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/launchdarkly.lua b/launchdarkly.lua index 6468fec..b8cdae1 100644 --- a/launchdarkly.lua +++ b/launchdarkly.lua @@ -235,6 +235,31 @@ function fromLaunchDarklyJSON(x) return cjson.decode(native) end +function convertDetails(cDetails, value) + local details = {} + local cReasonJSON = ld.so.LDReasonToJSON(cDetails) + details.reason = fromLaunchDarklyJSON(cReasonJSON) + + if cDetails.hasVariation then + details.variationIndex = cDetails.variationIndex + end + + details.value = value + return details +end + +function genericVariationDetail(client, user, key, fallback, variation, valueConverter) + local cDetails = ffi.new("struct LDDetails") + ld.so.LDDetailsInit(cDetails) + local value = variation(client, user, key, fallback, cDetails) + if valueConverter ~= nil then + value = valueConverter(value) + end + local details = convertDetails(cDetails, value) + ld.so.LDDetailsClear(cDetails) + return details +end + local makeConfig = function(fields) local config = ld.so.LDConfigNew(fields["key"]) @@ -341,14 +366,26 @@ ld.clientInit = function(config, timoutMilliseconds) return ld.so.LDBoolVariation(client, user, key, fallback, nil) end + interface.boolVariationDetail = function(user, key, fallback) + return genericVariationDetail(client, user, key, fallback, ld.so.LDBoolVariation, nil) + end + interface.intVariation = function(user, key, fallback) return ld.so.LDIntVariation(client, user, key, fallback, nil) end + interface.intVariationDetail = function(user, key, fallback) + return genericVariationDetail(client, user, key, fallback, ld.so.LDIntVariation, nil) + end + interface.doubleVariation = function(user, key, fallback) return ld.so.LDDoubleVariation(client, user, key, fallback, nil) end + interface.doubleVariationDetail = function(user, key, fallback) + return genericVariationDetail(client, user, key, fallback, ld.so.LDDoubleVariation, nil) + end + interface.stringVariation = function(user, key, fallback) local raw = ld.so.LDStringVariation(client, user, key, fallback, nil) local native = ffi.string(raw) @@ -356,10 +393,31 @@ ld.clientInit = function(config, timoutMilliseconds) return native end + interface.stringVariationDetail = function(user, key, fallback) + local valueConverter = function(raw) + local native = ffi.string(raw) + ld.so.LDFree(raw) + return native + end + + return genericVariationDetail(client, user, key, fallback, ld.so.LDStringVariation, valueConverter) + end + interface.jsonVariation = function(user, key, fallback) - return fromLaunchDarklyJSON( - ld.so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) - ) + local raw = ld.so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) + local native = fromLaunchDarklyJSON(raw) + ld.so.LDJSONFree(raw) + return native + end + + interface.jsonVariationDetail = function(user, key, fallback) + local valueConverter = function(raw) + local native = fromLaunchDarklyJSON(raw) + ld.so.LDJSONFree(raw) + return native + end + + return genericVariationDetail(client, user, key, toLaunchDarklyJSON(fallback), ld.so.LDJSONVariation, valueConverter) end return interface From 4a001eb2d92dce7ec88d526473dad09c69af3b35 Mon Sep 17 00:00:00 2001 From: hroederld Date: Mon, 9 Mar 2020 11:25:19 -0700 Subject: [PATCH 03/25] [ch68367] prefix functions with local --- launchdarkly.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/launchdarkly.lua b/launchdarkly.lua index b8cdae1..34cbc55 100644 --- a/launchdarkly.lua +++ b/launchdarkly.lua @@ -214,28 +214,28 @@ ffi.cdef[[ ld.so = ffi.load("ldserverapi") -function applyWhenNotNil(subject, operation, value) +local function applyWhenNotNil(subject, operation, value) if value ~= nil and value ~= cjson.null then operation(subject, value) end end -function toLaunchDarklyJSON(x) +local function toLaunchDarklyJSON(x) return ffi.gc(ld.so.LDJSONDeserialize(cjson.encode(x)), ld.so.LDJSONFree) end -function toLaunchDarklyJSONTransfer(x) +local function toLaunchDarklyJSONTransfer(x) return ld.so.LDJSONDeserialize(cjson.encode(x)) end -function fromLaunchDarklyJSON(x) +local function fromLaunchDarklyJSON(x) local raw = ld.so.LDJSONSerialize(x) local native = ffi.string(raw) ld.so.LDFree(raw) return cjson.decode(native) end -function convertDetails(cDetails, value) +local function convertDetails(cDetails, value) local details = {} local cReasonJSON = ld.so.LDReasonToJSON(cDetails) details.reason = fromLaunchDarklyJSON(cReasonJSON) @@ -248,7 +248,7 @@ function convertDetails(cDetails, value) return details end -function genericVariationDetail(client, user, key, fallback, variation, valueConverter) +local function genericVariationDetail(client, user, key, fallback, variation, valueConverter) local cDetails = ffi.new("struct LDDetails") ld.so.LDDetailsInit(cDetails) local value = variation(client, user, key, fallback, cDetails) @@ -260,7 +260,7 @@ function genericVariationDetail(client, user, key, fallback, variation, valueCon return details end -local makeConfig = function(fields) +local function makeConfig(fields) local config = ld.so.LDConfigNew(fields["key"]) applyWhenNotNil(config, ld.so.LDConfigSetBaseURI, fields["baseURI"]) From c66ed759f6633d04d09dea94a91fab21647e5408 Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 10 Mar 2020 15:04:29 -0700 Subject: [PATCH 04/25] [ch68610] Basic generated doc (#4) --- launchdarkly.lua | 328 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 262 insertions(+), 66 deletions(-) diff --git a/launchdarkly.lua b/launchdarkly.lua index 34cbc55..92ab778 100644 --- a/launchdarkly.lua +++ b/launchdarkly.lua @@ -1,8 +1,9 @@ +--- Server-side SDK for LaunchDarkly. +-- @module launchdarkly-server-sdk + local ffi = require("ffi") local cjson = require("cjson") -local ld = {} - ffi.cdef[[ struct LDJSON; typedef enum { @@ -212,7 +213,7 @@ ffi.cdef[[ struct LDUser *const user); ]] -ld.so = ffi.load("ldserverapi") +local so = ffi.load("ldserverapi") local function applyWhenNotNil(subject, operation, value) if value ~= nil and value ~= cjson.null then @@ -221,23 +222,40 @@ local function applyWhenNotNil(subject, operation, value) end local function toLaunchDarklyJSON(x) - return ffi.gc(ld.so.LDJSONDeserialize(cjson.encode(x)), ld.so.LDJSONFree) + return ffi.gc(so.LDJSONDeserialize(cjson.encode(x)), so.LDJSONFree) end local function toLaunchDarklyJSONTransfer(x) - return ld.so.LDJSONDeserialize(cjson.encode(x)) + return so.LDJSONDeserialize(cjson.encode(x)) end local function fromLaunchDarklyJSON(x) - local raw = ld.so.LDJSONSerialize(x) + local raw = so.LDJSONSerialize(x) local native = ffi.string(raw) - ld.so.LDFree(raw) + so.LDFree(raw) return cjson.decode(native) end +--- Details associated with an evaluation +-- @name Details +-- @class table +-- @tfield[opt] int variationIndex The index of the returned value within the +-- flag's list of variations. +-- @field value The resulting value of an evaluation +-- @tfield table reason The reason a specific value was returned +-- @tfield string reason.kind The kind of reason +-- @tfield[opt] string reason.errorKind If the kind is LD_ERROR, this contains +-- the error string. +-- @tfield[opt] string reason.ruleId If the kind is LD_RULE_MATCH this contains +-- the id of the rule. +-- @tfield[opt] int reason.ruleIndex If the kind is LD_RULE_MATCH this contains +-- the index of the rule. +-- @tfield[opt] string reason.prerequisiteKey If the kind is +-- LD_PREREQUISITE_FAILED this contains the key of the failed prerequisite. + local function convertDetails(cDetails, value) local details = {} - local cReasonJSON = ld.so.LDReasonToJSON(cDetails) + local cReasonJSON = so.LDReasonToJSON(cDetails) details.reason = fromLaunchDarklyJSON(cReasonJSON) if cDetails.hasVariation then @@ -250,95 +268,191 @@ end local function genericVariationDetail(client, user, key, fallback, variation, valueConverter) local cDetails = ffi.new("struct LDDetails") - ld.so.LDDetailsInit(cDetails) + so.LDDetailsInit(cDetails) local value = variation(client, user, key, fallback, cDetails) if valueConverter ~= nil then value = valueConverter(value) end local details = convertDetails(cDetails, value) - ld.so.LDDetailsClear(cDetails) + so.LDDetailsClear(cDetails) return details end +--- make a config local function makeConfig(fields) - local config = ld.so.LDConfigNew(fields["key"]) - - applyWhenNotNil(config, ld.so.LDConfigSetBaseURI, fields["baseURI"]) - applyWhenNotNil(config, ld.so.LDConfigSetStreamURI, fields["streamURI"]) - applyWhenNotNil(config, ld.so.LDConfigSetEventsURI, fields["eventsURI"]) - applyWhenNotNil(config, ld.so.LDConfigSetStream, fields["stream"]) - applyWhenNotNil(config, ld.so.LDConfigSetSendEvents, fields["sendEvents"]) - applyWhenNotNil(config, ld.so.LDConfigSetEventsCapacity, fields["eventsCapacity"]) - applyWhenNotNil(config, ld.so.LDConfigSetTimeout, fields["timeout"]) - applyWhenNotNil(config, ld.so.LDConfigSetFlushInterval, fields["flushInterval"]) - applyWhenNotNil(config, ld.so.LDConfigSetPollInterval, fields["pollInterval"]) - applyWhenNotNil(config, ld.so.LDConfigSetOffline, fields["offline"]) - applyWhenNotNil(config, ld.so.LDConfigSetAllAttributesPrivate, fields["allAttributesPrivate"]) - applyWhenNotNil(config, ld.so.LDConfigInlineUsersInEvents, fields["inlineUsersInEvents"]) - applyWhenNotNil(config, ld.so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) - applyWhenNotNil(config, ld.so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) + local config = so.LDConfigNew(fields["key"]) + + applyWhenNotNil(config, so.LDConfigSetBaseURI, fields["baseURI"]) + applyWhenNotNil(config, so.LDConfigSetStreamURI, fields["streamURI"]) + applyWhenNotNil(config, so.LDConfigSetEventsURI, fields["eventsURI"]) + applyWhenNotNil(config, so.LDConfigSetStream, fields["stream"]) + applyWhenNotNil(config, so.LDConfigSetSendEvents, fields["sendEvents"]) + applyWhenNotNil(config, so.LDConfigSetEventsCapacity, fields["eventsCapacity"]) + applyWhenNotNil(config, so.LDConfigSetTimeout, fields["timeout"]) + applyWhenNotNil(config, so.LDConfigSetFlushInterval, fields["flushInterval"]) + applyWhenNotNil(config, so.LDConfigSetPollInterval, fields["pollInterval"]) + applyWhenNotNil(config, so.LDConfigSetOffline, fields["offline"]) + applyWhenNotNil(config, so.LDConfigSetAllAttributesPrivate, fields["allAttributesPrivate"]) + applyWhenNotNil(config, so.LDConfigInlineUsersInEvents, fields["inlineUsersInEvents"]) + applyWhenNotNil(config, so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) + applyWhenNotNil(config, so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) local names = fields["privateAttributeNames"] if names ~= nil and names ~= cjson.null then for _, v in ipairs(names) do - ld.so.LDConfigAddPrivateAttribute(config, v) + so.LDConfigAddPrivateAttribute(config, v) end end return config end -ld.makeUser = function(fields) - local user = ffi.gc(ld.so.LDUserNew(fields["key"]), ld.so.LDUserFree) - - applyWhenNotNil(user, ld.so.LDUserSetAnonymous, fields["anonymous"]) - applyWhenNotNil(user, ld.so.LDUserSetIP, fields["ip"]) - applyWhenNotNil(user, ld.so.LDUserSetFirstName, fields["firstName"]) - applyWhenNotNil(user, ld.so.LDUserSetLastName, fields["lastName"]) - applyWhenNotNil(user, ld.so.LDUserSetEmail, fields["email"]) - applyWhenNotNil(user, ld.so.LDUserSetName, fields["name"]) - applyWhenNotNil(user, ld.so.LDUserSetAvatar, fields["avatar"]) - applyWhenNotNil(user, ld.so.LDUserSetCountry, fields["country"]) - applyWhenNotNil(user, ld.so.LDUserSetSecondary, fields["secondary"]) +--- Create a new opaque user object. +-- @tparam table fields list of user fields. +-- @tparam string fields.key The user's key +-- @tparam[opt] boolean fields.anonymous Mark the user as anonymous +-- @tparam[opt] string fields.ip Set the user's IP +-- @tparam[opt] string fields.firstName Set the user's first name +-- @tparam[opt] string fields.lastName Set the user's last name +-- @tparam[opt] string fields.email Set the user's email +-- @tparam[opt] string fields.name Set the user's name +-- @tparam[opt] string fields.avatar Set the user's avatar +-- @tparam[opt] string fields.country Set the user's country +-- @tparam[opt] string fields.secondary Set the user's secondary key +-- @tparam[opt] table fields.privateAttributeNames A list of attributes to +-- redact +-- @tparam[opt] table fields.custom Set the user's custom JSON +-- @return an opaque user object +local function makeUser(fields) + local user = ffi.gc(so.LDUserNew(fields["key"]), so.LDUserFree) + + applyWhenNotNil(user, so.LDUserSetAnonymous, fields["anonymous"]) + applyWhenNotNil(user, so.LDUserSetIP, fields["ip"]) + applyWhenNotNil(user, so.LDUserSetFirstName, fields["firstName"]) + applyWhenNotNil(user, so.LDUserSetLastName, fields["lastName"]) + applyWhenNotNil(user, so.LDUserSetEmail, fields["email"]) + applyWhenNotNil(user, so.LDUserSetName, fields["name"]) + applyWhenNotNil(user, so.LDUserSetAvatar, fields["avatar"]) + applyWhenNotNil(user, so.LDUserSetCountry, fields["country"]) + applyWhenNotNil(user, so.LDUserSetSecondary, fields["secondary"]) if fields["custom"] ~= nil then - ld.so.LDUserSetCustom(user, toLaunchDarklyJSONTransfer(fields["custom"])) + so.LDUserSetCustom(user, toLaunchDarklyJSONTransfer(fields["custom"])) end local names = fields["privateAttributeNames"] if names ~= nil and names ~= cjson.null then for _, v in ipairs(names) do - ngx.log(ngx.ERR, "value: " .. v) - ld.so.LDUserAddPrivateAttribute(user, v) + so.LDUserAddPrivateAttribute(user, v) end end return user end -ld.clientInit = function(config, timoutMilliseconds) +--- Initialize a new client, and connect to LaunchDarkly. +-- @tparam table config list of configuration options +-- @tparam string config.key Environment SDK key +-- @tparam[opt] string config.baseURI Set the base URI for connecting to +-- LaunchDarkly. You probably don't need to set this unless instructed by +-- LaunchDarkly. +-- @tparam[opt] string config.streamURI Set the streaming URI for connecting to +-- LaunchDarkly. You probably don't need to set this unless instructed by +-- LaunchDarkly. +-- @tparam[opt] string config.eventsURI Set the events URI for connecting to +-- LaunchDarkly. You probably don't need to set this unless instructed by +-- LaunchDarkly. +-- @tparam[opt] boolean config.stream Enables or disables real-time streaming +-- flag updates. When set to false, an efficient caching polling mechanism is +-- used. We do not recommend disabling streaming unless you have been instructed +-- to do so by LaunchDarkly support. Defaults to true. +-- @tparam[opt] string config.sendEvents Sets whether to send analytics events +-- back to LaunchDarkly. By default, the client will send events. This differs +-- from Offline in that it only affects sending events, not streaming or +-- polling. +-- @tparam[opt] int config.eventsCapacity The capacity of the events buffer. +-- The client buffers up to this many events in memory before flushing. If the +-- capacity is exceeded before the buffer is flushed, events will be discarded. +-- @tparam[opt] int config.timeout The connection timeout to use when making +-- requests to LaunchDarkly. +-- @tparam[opt] int config.flushInterval he time between flushes of the event +-- buffer. Decreasing the flush interval means that the event buffer is less +-- likely to reach capacity. +-- @tparam[opt] int config.pollInterval The polling interval +-- (when streaming is disabled) in milliseconds. +-- @tparam[opt] boolean config.offline Sets whether this client is offline. +-- An offline client will not make any network connections to LaunchDarkly, +-- and will return default values for all feature flags. +-- @tparam[opt] boolean config.allAttributesPrivate Sets whether or not all user +-- attributes (other than the key) should be hidden from LaunchDarkly. If this +-- is true, all user attribute values will be private, not just the attributes +-- specified in PrivateAttributeNames. +-- @tparam[opt] boolean config.inlineUsersInEvents Set to true if you need to +-- see the full user details in every analytics event. +-- @tparam[opt] int config.userKeysCapacity The number of user keys that the +-- event processor can remember at an one time, so that duplicate user details +-- will not be sent in analytics. +-- @tparam[opt] int config.userKeysFlushInterval The interval at which the event +-- processor will reset its set of known user keys, in milliseconds. +-- @tparam[opt] table config.privateAttributeNames Marks a set of user attribute +-- names private. Any users sent to LaunchDarkly with this configuration active +-- will have attributes with these names removed. +-- @tparam int timeoutMilliseconds How long to wait for flags to download. +-- If the timeout is reached a non fully initialized client will be returned. +-- @return A fresh client. +local function clientInit(config, timeoutMilliseconds) local interface = {} - local client = ffi.gc(ld.so.LDClientInit(makeConfig(config), 1000), ld.so.LDClientClose) + --- An opaque client object + -- @type Client + + local client = ffi.gc(so.LDClientInit(makeConfig(config), 1000), so.LDClientClose) + --- Check if a client has been fully initialized. This may be useful if the + -- initialization timeout was reached. + -- @class function + -- @name isInitialized + -- @treturn boolean true if fully initialized interface.isInitialized = function() - return ld.so.LDClientIsInitialized(client) + return so.LDClientIsInitialized(client) end + --- Generates an identify event for a user. + -- @class function + -- @name identify + -- @tparam user user An opaque user object from @{makeUser} + -- @treturn nil interface.identify = function(user) - ld.so.LDClientIdentify(client, user) + so.LDClientIdentify(client, user) end + --- Whether the LaunchDarkly client is in offline mode. + -- @class function + -- @name isOffline + -- @treturn boolean true if offline interface.isOffline = function() - ld.so.LDClientIsOffline(client) + so.LDClientIsOffline(client) end + --- Immediately flushes queued events. + -- @class function + -- @name flush + -- @treturn nil interface.flush = function() - ld.so.LDClientFlush(client) + so.LDClientFlush(client) end + --- Reports that a user has performed an event. Custom data, and a metric + -- can be attached to the event as JSON. + -- @class function + -- @name track + -- @tparam string key The name of the event + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam[opt] table data A value to be associated with an event + -- @tparam[optchain] number metric A value to be associated with an event + -- @treturn nil interface.track = function(key, user, data, metric) local json = nil @@ -347,14 +461,20 @@ ld.clientInit = function(config, timoutMilliseconds) end if metric ~= nil then - ld.so.LDClientTrackMetric(client, key, user, json, metric) + so.LDClientTrackMetric(client, key, user, json, metric) else - ld.so.LDClientTrack(client, key, user, json) + so.LDClientTrack(client, key, user, json) end end + --- Returns a map from feature flag keys to values for a given user. + -- This does not send analytics events back to LaunchDarkly. + -- @class function + -- @name allFlags + -- @tparam user user An opaque user object from @{makeUser} + -- @treturn table interface.allFlags = function(user) - local x = ld.so.LDAllFlags(client, user) + local x = so.LDAllFlags(client, user) if x ~= nil then return fromLaunchDarklyJSON(x) else @@ -362,65 +482,141 @@ ld.clientInit = function(config, timoutMilliseconds) end end + --- Evaluate a boolean flag + -- @class function + -- @name boolVariation + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam boolean fallback The value to return on error + -- @treturn boolean The evaluation result, or the fallback value interface.boolVariation = function(user, key, fallback) - return ld.so.LDBoolVariation(client, user, key, fallback, nil) + return so.LDBoolVariation(client, user, key, fallback, nil) end + --- Evaluate a boolean flag and return an explanation + -- @class function + -- @name boolVariationDetail + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam boolean fallback The value to return on error + -- @treturn table The evaluation explanation interface.boolVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, ld.so.LDBoolVariation, nil) + return genericVariationDetail(client, user, key, fallback, so.LDBoolVariation, nil) end + --- Evaluate an integer flag + -- @class function + -- @name intVariation + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam int fallback The value to return on error + -- @treturn int The evaluation result, or the fallback value interface.intVariation = function(user, key, fallback) - return ld.so.LDIntVariation(client, user, key, fallback, nil) + return so.LDIntVariation(client, user, key, fallback, nil) end + --- Evaluate an integer flag and return an explanation + -- @class function + -- @name intVariationDetail + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam int fallback The value to return on error + -- @treturn table The evaluation explanation interface.intVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, ld.so.LDIntVariation, nil) + return genericVariationDetail(client, user, key, fallback, so.LDIntVariation, nil) end + --- Evaluate a double flag + -- @class function + -- @name doubleVariation + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam number fallback The value to return on error + -- @treturn double The evaluation result, or the fallback value interface.doubleVariation = function(user, key, fallback) - return ld.so.LDDoubleVariation(client, user, key, fallback, nil) + return so.LDDoubleVariation(client, user, key, fallback, nil) end + --- Evaluate a double flag and return an explanation + -- @class function + -- @name doubleVariationDetail + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam number fallback The value to return on error + -- @treturn table The evaluation explanation interface.doubleVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, ld.so.LDDoubleVariation, nil) + return genericVariationDetail(client, user, key, fallback, so.LDDoubleVariation, nil) end + --- Evaluate a string flag + -- @class function + -- @name stringVariation + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam string fallback The value to return on error + -- @treturn string The evaluation result, or the fallback value interface.stringVariation = function(user, key, fallback) - local raw = ld.so.LDStringVariation(client, user, key, fallback, nil) + local raw = so.LDStringVariation(client, user, key, fallback, nil) local native = ffi.string(raw) - ld.so.LDFree(raw) + so.LDFree(raw) return native end + --- Evaluate a string flag and return an explanation + -- @class function + -- @name stringVariationDetail + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam string fallback The value to return on error + -- @treturn table The evaluation explanation interface.stringVariationDetail = function(user, key, fallback) local valueConverter = function(raw) local native = ffi.string(raw) - ld.so.LDFree(raw) + so.LDFree(raw) return native end - return genericVariationDetail(client, user, key, fallback, ld.so.LDStringVariation, valueConverter) + return genericVariationDetail(client, user, key, fallback, so.LDStringVariation, valueConverter) end + --- Evaluate a json flag + -- @class function + -- @name jsonVariation + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam table fallback The value to return on error + -- @treturn table The evaluation result, or the fallback value interface.jsonVariation = function(user, key, fallback) - local raw = ld.so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) + local raw = so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) local native = fromLaunchDarklyJSON(raw) - ld.so.LDJSONFree(raw) + so.LDJSONFree(raw) return native end + --- Evaluate a json flag and return an explanation + -- @class function + -- @name jsonVariationDetail + -- @tparam user user An opaque user object from @{makeUser} + -- @tparam string key The key of the flag to evaluate. + -- @tparam table fallback The value to return on error + -- @treturn table The evaluation explanation interface.jsonVariationDetail = function(user, key, fallback) local valueConverter = function(raw) local native = fromLaunchDarklyJSON(raw) - ld.so.LDJSONFree(raw) + so.LDJSONFree(raw) return native end - return genericVariationDetail(client, user, key, toLaunchDarklyJSON(fallback), ld.so.LDJSONVariation, valueConverter) + return genericVariationDetail(client, user, key, toLaunchDarklyJSON(fallback), so.LDJSONVariation, valueConverter) end + --- @type end + return interface end -return ld +--- @export +return { + makeUser = makeUser, + clientInit = clientInit +} From e69359c5e1769376528a1ef95c3911b95fdb4f2d Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 18 Mar 2020 19:49:46 -0700 Subject: [PATCH 05/25] [ch70062] rename to launchdarkly-server-sdk.lua --- launchdarkly.lua => launchdarkly-server-sdk.lua | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename launchdarkly.lua => launchdarkly-server-sdk.lua (100%) diff --git a/launchdarkly.lua b/launchdarkly-server-sdk.lua similarity index 100% rename from launchdarkly.lua rename to launchdarkly-server-sdk.lua From 24b948718a4147837aad984a40b5f8e706bb2d0e Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 24 Mar 2020 14:51:23 -0700 Subject: [PATCH 06/25] [ch70700] prepare 1.0.0-beta.1 --- CHANGELOG.md | 7 +++++++ LICENSE | 13 +++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..830fc1a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Change log + +All notable changes to the LaunchDarkly Lua Server-side SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). + +## [1.0.0-beta.1] - 2020-03-24 + +Initial beta release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f711cb7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2020 Catamorphic, Co. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. From 6ed903a8666e0f9f2187a687b7e936cfaf6c55ba Mon Sep 17 00:00:00 2001 From: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Date: Tue, 24 Mar 2020 14:52:08 -0700 Subject: [PATCH 07/25] Update CONTRIBUTING.md (#8) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb41d17..51ae91c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -Contributing to the LaunchDarkly Server SDK for Lua +Contributing to the LaunchDarkly Server-Side SDK for Lua ================================================ LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. From 17a35ef00027549577e7fdb15b09121ca569b960 Mon Sep 17 00:00:00 2001 From: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Date: Tue, 24 Mar 2020 14:52:14 -0700 Subject: [PATCH 08/25] Update README.md (#7) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb7f090..4d2483a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -LaunchDarkly server SDK for Lua +LaunchDarkly Server-Side SDK for Lua =========================== [![Circle CI](https://circleci.com/gh/launchdarkly/lua-server-sdk.svg?style=shield)](https://circleci.com/gh/launchdarkly/lua-server-sdk) From 791dd2e36c2ed8392210c86f7f52850cea3069c7 Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 25 Mar 2020 12:51:51 -0700 Subject: [PATCH 09/25] remove circle badge (#9) --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4d2483a..6585ef9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ LaunchDarkly Server-Side SDK for Lua =========================== -[![Circle CI](https://circleci.com/gh/launchdarkly/lua-server-sdk.svg?style=shield)](https://circleci.com/gh/launchdarkly/lua-server-sdk) - *This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* LaunchDarkly overview @@ -14,12 +12,12 @@ LaunchDarkly overview Getting started ----------- -Refer to the [SDK documentation](https://docs.launchdarkly.com/docs/lua-server-reference#section-getting-started) for instructions on getting started with using the SDK. +Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/server-side/lua#getting-started) for instructions on getting started with using the SDK. Learn more ----------- -Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/docs/lua-server-reference). +Check out our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/lua). Testing ------- From b1f585cf378d8a0c7e155e7950690baff60d1707 Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 1 Apr 2020 10:32:36 -0700 Subject: [PATCH 10/25] [ch70699] releaser support (#11) --- .circleci/config.yml | 20 ++++++++++++++++++++ .ldrelease/config.yml | 14 ++++++++++++++ .ldrelease/linux-build-docs.sh | 13 +++++++++++++ .ldrelease/linux-prepare.sh | 5 +++++ 4 files changed, 52 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 .ldrelease/config.yml create mode 100755 .ldrelease/linux-build-docs.sh create mode 100755 .ldrelease/linux-prepare.sh diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..590166b --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,20 @@ +version: 2.1 + +workflows: + version: 2 + all: + jobs: + - build-doc-linux + +jobs: + build-doc-linux: + docker: + - image: ubuntu:18.04 + steps: + - checkout + - run: + name: Prepare + command: ./.ldrelease/linux-prepare.sh + - run: + name: Build Doc + command: ./.ldrelease/linux-build-docs.sh diff --git a/.ldrelease/config.yml b/.ldrelease/config.yml new file mode 100644 index 0000000..32e5ac5 --- /dev/null +++ b/.ldrelease/config.yml @@ -0,0 +1,14 @@ +repo: + public: lua-server-sdk + private: lua-server-sdk-private + +circleci: + linux: + image: ubuntu:18.04 + +documentation: + title: LaunchDarkly Server-Side SDK for Lua + githubPages: true + +sdk: + displayName: "Lua (server-side)" diff --git a/.ldrelease/linux-build-docs.sh b/.ldrelease/linux-build-docs.sh new file mode 100755 index 0000000..91557bb --- /dev/null +++ b/.ldrelease/linux-build-docs.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +# This only runs in the Linux build, since the docs are the same for all platforms. + +PROJECT_DIR=$(pwd) + +ldoc launchdarkly-server-sdk.lua + +mkdir -p $PROJECT_DIR/artifacts +cd $PROJECT_DIR/doc +zip -r $PROJECT_DIR/artifacts/docs.zip * diff --git a/.ldrelease/linux-prepare.sh b/.ldrelease/linux-prepare.sh new file mode 100755 index 0000000..d3d8578 --- /dev/null +++ b/.ldrelease/linux-prepare.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +apt-get update -y && apt-get install -y lua-ldoc zip ca-certificates From 3ab1ee2752db040d967d2a768ffaec23a12d113e Mon Sep 17 00:00:00 2001 From: hroederld Date: Thu, 16 Apr 2020 18:38:51 -0700 Subject: [PATCH 11/25] [ch73979] document version constraints (#12) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 6585ef9..a5fe5b4 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,12 @@ LaunchDarkly overview [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) + +Supported Lua versions +----------- + +This version of the LaunchDarkly SDK is compatible with the Lua 5.1 interpreter, and LuaJIT. Lua 5.3 is not supported due to FFI constraints. + Getting started ----------- From f2ed7f178b60447980c4666453dad7b2819f5f3e Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 12 May 2020 08:41:59 -0700 Subject: [PATCH 12/25] [ch76282] add wrapper meta config (#13) --- .ldrelease/update-version.sh | 5 +++++ launchdarkly-server-sdk.lua | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100755 .ldrelease/update-version.sh diff --git a/.ldrelease/update-version.sh b/.ldrelease/update-version.sh new file mode 100755 index 0000000..64d347a --- /dev/null +++ b/.ldrelease/update-version.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -e + +sed -i "s/local SDKVersion =.*/local SDKVersion = \"${LD_RELEASE_VERSION}\"/" 'launchdarkly-server-sdk.lua' diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua index 92ab778..15d8bfa 100644 --- a/launchdarkly-server-sdk.lua +++ b/launchdarkly-server-sdk.lua @@ -91,6 +91,8 @@ ffi.cdef[[ const char *const attribute); void LDConfigSetFeatureStoreBackend(struct LDConfig *const config, struct LDStoreInterface *const backend); + bool LDConfigSetWrapperInfo(struct LDConfig *const config, + const char *const wrapperName, const char *const wrapperVersion); struct LDUser * LDUserNew(const char *const userkey); void LDUserFree(struct LDUser *const user); void LDUserSetAnonymous(struct LDUser *const user, const bool anon); @@ -213,6 +215,8 @@ ffi.cdef[[ struct LDUser *const user); ]] +local SDKVersion = "1.0.0-beta.1" + local so = ffi.load("ldserverapi") local function applyWhenNotNil(subject, operation, value) @@ -297,6 +301,8 @@ local function makeConfig(fields) applyWhenNotNil(config, so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) applyWhenNotNil(config, so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) + so.LDConfigSetWrapperInfo(config, "lua-server-sdk", SDKVersion) + local names = fields["privateAttributeNames"] if names ~= nil and names ~= cjson.null then From 57517c18a4dedc530712a09ef04a7cd362240404 Mon Sep 17 00:00:00 2001 From: Harpo Roeder Date: Tue, 12 May 2020 14:11:44 -0700 Subject: [PATCH 13/25] prepare 1.0.0-beta.2 (content approved in previous failed release PR) --- CHANGELOG.md | 5 +++++ launchdarkly-server-sdk.lua | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 830fc1a..9331c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to the LaunchDarkly Lua Server-side SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [1.0.0-beta.2] - 2020-05-12 + +### Changed: +- Updates the configuration object to include wrapper name and version. + ## [1.0.0-beta.1] - 2020-03-24 Initial beta release. diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua index 15d8bfa..bb72c73 100644 --- a/launchdarkly-server-sdk.lua +++ b/launchdarkly-server-sdk.lua @@ -215,7 +215,7 @@ ffi.cdef[[ struct LDUser *const user); ]] -local SDKVersion = "1.0.0-beta.1" +local SDKVersion = "1.0.0-beta.2" local so = ffi.load("ldserverapi") From bf8fd31ff12194855769dbc8e91f3b3ab7d24f64 Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 23 Jun 2020 13:35:45 -0700 Subject: [PATCH 14/25] [ch80640] logging handler (#14) --- launchdarkly-server-sdk.lua | 40 +++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua index bb72c73..06c4790 100644 --- a/launchdarkly-server-sdk.lua +++ b/launchdarkly-server-sdk.lua @@ -282,6 +282,41 @@ local function genericVariationDetail(client, user, key, fallback, variation, va return details end +local function stringToLogLevel(level) + local translation = { + ["FATAL"] = so.LD_LOG_FATAL, + ["CRITICAL"] = so.LD_LOG_CRITICAL, + ["ERROR"] = so.LD_LOG_ERROR, + ["WARNING"] = so.LD_LOG_WARNING, + ["INFO"] = so.LD_LOG_INFO, + ["DEBUG"] = so.LD_LOG_DEBUG, + ["TRACE"] = so.LD_LOG_TRACE + } + + local lookup = translation[level] + + if lookup == nil then + return so.LD_LOG_INFO + else + return lookup + end +end + +--- Set the global logger for all SDK operations. This function is not thread +-- safe, and if used should be done so before other operations. The default +-- log level is "INFO". +-- @tparam string logLevel The level to at. Available options are: +-- "FATAL", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE". +-- @tparam function cb The logging handler. Callback must be of the form +-- "function (logLevel, logLine) ... end". +local function registerLogger(logLevel, cb) + so.LDConfigureGlobalLogger(stringToLogLevel(logLevel), + function(logLevel, line) + cb(ffi.string(so.LDLogLevelToString(logLevel)), ffi.string(line)) + end + ) +end + --- make a config local function makeConfig(fields) local config = so.LDConfigNew(fields["key"]) @@ -623,6 +658,7 @@ end --- @export return { - makeUser = makeUser, - clientInit = clientInit + registerLogger = registerLogger, + makeUser = makeUser, + clientInit = clientInit } From 77e52a1a1bb5e73a7ce2cbcc497944e41da50370 Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 23 Jun 2020 15:32:01 -0700 Subject: [PATCH 15/25] [ch80816] basic unit tests (#15) --- .circleci/config.yml | 10 +- .ldrelease/linux-prepare.sh | 3 +- attribution/luaunit.txt | 12 + luaunit.lua | 3263 +++++++++++++++++++++++++++++++++++ scripts/fetch-linux.sh | 8 + test.lua | 39 + 6 files changed, 3332 insertions(+), 3 deletions(-) create mode 100644 attribution/luaunit.txt create mode 100644 luaunit.lua create mode 100755 scripts/fetch-linux.sh create mode 100644 test.lua diff --git a/.circleci/config.yml b/.circleci/config.yml index 590166b..da41be4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,10 +4,10 @@ workflows: version: 2 all: jobs: - - build-doc-linux + - build-test-linux jobs: - build-doc-linux: + build-test-linux: docker: - image: ubuntu:18.04 steps: @@ -18,3 +18,9 @@ jobs: - run: name: Build Doc command: ./.ldrelease/linux-build-docs.sh + - run: + name: Fetch c-server-sdk + command: ./scripts/fetch-linux.sh + - run: + name: Run tests + command: LD_LIBRARY_PATH=./lib luajit test.lua diff --git a/.ldrelease/linux-prepare.sh b/.ldrelease/linux-prepare.sh index d3d8578..b9e7621 100755 --- a/.ldrelease/linux-prepare.sh +++ b/.ldrelease/linux-prepare.sh @@ -2,4 +2,5 @@ set -e -apt-get update -y && apt-get install -y lua-ldoc zip ca-certificates +apt-get update -y && apt-get install -y luajit lua-ldoc zip ca-certificates \ + curl zip lua-cjson libpcre3 libcurl4-openssl-dev diff --git a/attribution/luaunit.txt b/attribution/luaunit.txt new file mode 100644 index 0000000..6f414b1 --- /dev/null +++ b/attribution/luaunit.txt @@ -0,0 +1,12 @@ +This software is distributed under the BSD License. + +Copyright (c) 2005-2018, Philippe Fremy + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/luaunit.lua b/luaunit.lua new file mode 100644 index 0000000..6753f13 --- /dev/null +++ b/luaunit.lua @@ -0,0 +1,3263 @@ +--[[ + luaunit.lua + +Description: A unit testing framework +Homepage: https://github.com/bluebird75/luaunit +Development by Philippe Fremy +Based on initial work of Ryu, Gwang (http://www.gpgstudy.com/gpgiki/LuaUnit) +License: BSD License, see LICENSE.txt +]]-- + +require("math") +local M={} + +-- private exported functions (for testing) +M.private = {} + +M.VERSION='3.4-dev' +M._VERSION=M.VERSION -- For LuaUnit v2 compatibility + +-- a version which distinguish between regular Lua and LuaJit +M._LUAVERSION = (jit and jit.version) or _VERSION + +--[[ Some people like assertEquals( actual, expected ) and some people prefer +assertEquals( expected, actual ). +]]-- +M.ORDER_ACTUAL_EXPECTED = true +M.PRINT_TABLE_REF_IN_ERROR_MSG = false +M.LINE_LENGTH = 80 +M.TABLE_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items +M.LIST_DIFF_ANALYSIS_THRESHOLD = 10 -- display deep analysis for more than 10 items + +--[[ EPS is meant to help with Lua's floating point math in simple corner +cases like almostEquals(1.1-0.1, 1), which may not work as-is (e.g. on numbers +with rational binary representation) if the user doesn't provide some explicit +error margin. + +The default margin used by almostEquals() in such cases is EPS; and since +Lua may be compiled with different numeric precisions (single vs. double), we +try to select a useful default for it dynamically. Note: If the initial value +is not acceptable, it can be changed by the user to better suit specific needs. + +See also: https://en.wikipedia.org/wiki/Machine_epsilon +]] +M.EPS = 2^-52 -- = machine epsilon for "double", ~2.22E-16 +if math.abs(1.1 - 1 - 0.1) > M.EPS then + -- rounding error is above EPS, assume single precision + M.EPS = 2^-23 -- = machine epsilon for "float", ~1.19E-07 +end + +-- set this to false to debug luaunit +local STRIP_LUAUNIT_FROM_STACKTRACE = true + +M.VERBOSITY_DEFAULT = 10 +M.VERBOSITY_LOW = 1 +M.VERBOSITY_QUIET = 0 +M.VERBOSITY_VERBOSE = 20 +M.DEFAULT_DEEP_ANALYSIS = nil +M.FORCE_DEEP_ANALYSIS = true +M.DISABLE_DEEP_ANALYSIS = false + +-- set EXPORT_ASSERT_TO_GLOBALS to have all asserts visible as global values +-- EXPORT_ASSERT_TO_GLOBALS = true + +-- we need to keep a copy of the script args before it is overriden +local cmdline_argv = rawget(_G, "arg") + +M.FAILURE_PREFIX = 'LuaUnit test FAILURE: ' -- prefix string for failed tests +M.SUCCESS_PREFIX = 'LuaUnit test SUCCESS: ' -- prefix string for successful tests finished early +M.SKIP_PREFIX = 'LuaUnit test SKIP: ' -- prefix string for skipped tests + + + +M.USAGE=[[Usage: lua [options] [testname1 [testname2] ... ] +Options: + -h, --help: Print this help + --version: Print version information + -v, --verbose: Increase verbosity + -q, --quiet: Set verbosity to minimum + -e, --error: Stop on first error + -f, --failure: Stop on first failure or error + -s, --shuffle: Shuffle tests before running them + -o, --output OUTPUT: Set output type to OUTPUT + Possible values: text, tap, junit, nil + -n, --name NAME: For junit only, mandatory name of xml file + -r, --repeat NUM: Execute all tests NUM times, e.g. to trig the JIT + -p, --pattern PATTERN: Execute all test names matching the Lua PATTERN + May be repeated to include several patterns + Make sure you escape magic chars like +? with % + -x, --exclude PATTERN: Exclude all test names matching the Lua PATTERN + May be repeated to exclude several patterns + Make sure you escape magic chars like +? with % + testname1, testname2, ... : tests to run in the form of testFunction, + TestClass or TestClass.testMethod +]] + +local is_equal -- defined here to allow calling from mismatchFormattingPureList + +---------------------------------------------------------------- +-- +-- general utility functions +-- +---------------------------------------------------------------- + +local function pcall_or_abort(func, ...) + -- unpack is a global function for Lua 5.1, otherwise use table.unpack + local unpack = rawget(_G, "unpack") or table.unpack + local result = {pcall(func, ...)} + if not result[1] then + -- an error occurred + print(result[2]) -- error message + print() + print(M.USAGE) + os.exit(-1) + end + return unpack(result, 2) +end + +local crossTypeOrdering = { + number = 1, boolean = 2, string = 3, table = 4, other = 5 +} +local crossTypeComparison = { + number = function(a, b) return a < b end, + string = function(a, b) return a < b end, + other = function(a, b) return tostring(a) < tostring(b) end, +} + +local function crossTypeSort(a, b) + local type_a, type_b = type(a), type(b) + if type_a == type_b then + local func = crossTypeComparison[type_a] or crossTypeComparison.other + return func(a, b) + end + type_a = crossTypeOrdering[type_a] or crossTypeOrdering.other + type_b = crossTypeOrdering[type_b] or crossTypeOrdering.other + return type_a < type_b +end + +local function __genSortedIndex( t ) + -- Returns a sequence consisting of t's keys, sorted. + local sortedIndex = {} + + for key,_ in pairs(t) do + table.insert(sortedIndex, key) + end + + table.sort(sortedIndex, crossTypeSort) + return sortedIndex +end +M.private.__genSortedIndex = __genSortedIndex + +local function sortedNext(state, control) + -- Equivalent of the next() function of table iteration, but returns the + -- keys in sorted order (see __genSortedIndex and crossTypeSort). + -- The state is a temporary variable during iteration and contains the + -- sorted key table (state.sortedIdx). It also stores the last index (into + -- the keys) used by the iteration, to find the next one quickly. + local key + + --print("sortedNext: control = "..tostring(control) ) + if control == nil then + -- start of iteration + state.count = #state.sortedIdx + state.lastIdx = 1 + key = state.sortedIdx[1] + return key, state.t[key] + end + + -- normally, we expect the control variable to match the last key used + if control ~= state.sortedIdx[state.lastIdx] then + -- strange, we have to find the next value by ourselves + -- the key table is sorted in crossTypeSort() order! -> use bisection + local lower, upper = 1, state.count + repeat + state.lastIdx = math.modf((lower + upper) / 2) + key = state.sortedIdx[state.lastIdx] + if key == control then + break -- key found (and thus prev index) + end + if crossTypeSort(key, control) then + -- key < control, continue search "right" (towards upper bound) + lower = state.lastIdx + 1 + else + -- key > control, continue search "left" (towards lower bound) + upper = state.lastIdx - 1 + end + until lower > upper + if lower > upper then -- only true if the key wasn't found, ... + state.lastIdx = state.count -- ... so ensure no match in code below + end + end + + -- proceed by retrieving the next value (or nil) from the sorted keys + state.lastIdx = state.lastIdx + 1 + key = state.sortedIdx[state.lastIdx] + if key then + return key, state.t[key] + end + + -- getting here means returning `nil`, which will end the iteration +end + +local function sortedPairs(tbl) + -- Equivalent of the pairs() function on tables. Allows to iterate in + -- sorted order. As required by "generic for" loops, this will return the + -- iterator (function), an "invariant state", and the initial control value. + -- (see http://www.lua.org/pil/7.2.html) + return sortedNext, {t = tbl, sortedIdx = __genSortedIndex(tbl)}, nil +end +M.private.sortedPairs = sortedPairs + +-- seed the random with a strongly varying seed +math.randomseed(os.clock()*1E11) + +local function randomizeTable( t ) + -- randomize the item orders of the table t + for i = #t, 2, -1 do + local j = math.random(i) + if i ~= j then + t[i], t[j] = t[j], t[i] + end + end +end +M.private.randomizeTable = randomizeTable + +local function strsplit(delimiter, text) +-- Split text into a list consisting of the strings in text, separated +-- by strings matching delimiter (which may _NOT_ be a pattern). +-- Example: strsplit(", ", "Anna, Bob, Charlie, Dolores") + if delimiter == "" or delimiter == nil then -- this would result in endless loops + error("delimiter is nil or empty string!") + end + if text == nil then + return nil + end + + local list, pos, first, last = {}, 1 + while true do + first, last = text:find(delimiter, pos, true) + if first then -- found? + table.insert(list, text:sub(pos, first - 1)) + pos = last + 1 + else + table.insert(list, text:sub(pos)) + break + end + end + return list +end +M.private.strsplit = strsplit + +local function hasNewLine( s ) + -- return true if s has a newline + return (string.find(s, '\n', 1, true) ~= nil) +end +M.private.hasNewLine = hasNewLine + +local function prefixString( prefix, s ) + -- Prefix all the lines of s with prefix + return prefix .. string.gsub(s, '\n', '\n' .. prefix) +end +M.private.prefixString = prefixString + +local function strMatch(s, pattern, start, final ) + -- return true if s matches completely the pattern from index start to index end + -- return false in every other cases + -- if start is nil, matches from the beginning of the string + -- if final is nil, matches to the end of the string + start = start or 1 + final = final or string.len(s) + + local foundStart, foundEnd = string.find(s, pattern, start, false) + return foundStart == start and foundEnd == final +end +M.private.strMatch = strMatch + +local function patternFilter(patterns, expr) + -- Run `expr` through the inclusion and exclusion rules defined in patterns + -- and return true if expr shall be included, false for excluded. + -- Inclusion pattern are defined as normal patterns, exclusions + -- patterns start with `!` and are followed by a normal pattern + + -- result: nil = UNKNOWN (not matched yet), true = ACCEPT, false = REJECT + -- default: true if no explicit "include" is found, set to false otherwise + local default, result = true, nil + + if patterns ~= nil then + for _, pattern in ipairs(patterns) do + local exclude = pattern:sub(1,1) == '!' + if exclude then + pattern = pattern:sub(2) + else + -- at least one include pattern specified, a match is required + default = false + end + -- print('pattern: ',pattern) + -- print('exclude: ',exclude) + -- print('default: ',default) + + if string.find(expr, pattern) then + -- set result to false when excluding, true otherwise + result = not exclude + end + end + end + + if result ~= nil then + return result + end + return default +end +M.private.patternFilter = patternFilter + +local function xmlEscape( s ) + -- Return s escaped for XML attributes + -- escapes table: + -- " " + -- ' ' + -- < < + -- > > + -- & & + + return string.gsub( s, '.', { + ['&'] = "&", + ['"'] = """, + ["'"] = "'", + ['<'] = "<", + ['>'] = ">", + } ) +end +M.private.xmlEscape = xmlEscape + +local function xmlCDataEscape( s ) + -- Return s escaped for CData section, escapes: "]]>" + return string.gsub( s, ']]>', ']]>' ) +end +M.private.xmlCDataEscape = xmlCDataEscape + + +local function lstrip( s ) + --[[Return s with all leading white spaces and tabs removed]] + local idx = 0 + while idx < s:len() do + idx = idx + 1 + local c = s:sub(idx,idx) + if c ~= ' ' and c ~= '\t' then + break + end + end + return s:sub(idx) +end +M.private.lstrip = lstrip + +local function extractFileLineInfo( s ) + --[[ From a string in the form "(leading spaces) dir1/dir2\dir3\file.lua:linenb: msg" + + Return the "file.lua:linenb" information + ]] + local s2 = lstrip(s) + local firstColon = s2:find(':', 1, true) + if firstColon == nil then + -- string is not in the format file:line: + return s + end + local secondColon = s2:find(':', firstColon+1, true) + if secondColon == nil then + -- string is not in the format file:line: + return s + end + + return s2:sub(1, secondColon-1) +end +M.private.extractFileLineInfo = extractFileLineInfo + + +local function stripLuaunitTrace2( stackTrace, errMsg ) + --[[ + -- Example of a traceback: + < + [C]: in function 'xpcall' + ./luaunit.lua:1449: in function 'protectedCall' + ./luaunit.lua:1508: in function 'execOneFunction' + ./luaunit.lua:1596: in function 'runSuiteByInstances' + ./luaunit.lua:1660: in function 'runSuiteByNames' + ./luaunit.lua:1736: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + Other example: + < + [C]: in function 'xpcall' + ./luaunit.lua:1517: in function 'protectedCall' + ./luaunit.lua:1578: in function 'execOneFunction' + ./luaunit.lua:1677: in function 'runSuiteByInstances' + ./luaunit.lua:1730: in function 'runSuiteByNames' + ./luaunit.lua:1806: in function 'runSuite' + example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + < + [C]: in function 'xpcall' + luaunit2/luaunit.lua:1532: in function 'protectedCall' + luaunit2/luaunit.lua:1591: in function 'execOneFunction' + luaunit2/luaunit.lua:1679: in function 'runSuiteByInstances' + luaunit2/luaunit.lua:1743: in function 'runSuiteByNames' + luaunit2/luaunit.lua:1819: in function 'runSuite' + luaunit2/example_with_luaunit.lua:140: in main chunk + [C]: in ?>> + error message: <> + + + -- first line is "stack traceback": KEEP + -- next line may be luaunit line: REMOVE + -- next lines are call in the program under testOk: REMOVE + -- next lines are calls from luaunit to call the program under test: KEEP + + -- Strategy: + -- keep first line + -- remove lines that are part of luaunit + -- kepp lines until we hit a luaunit line + + The strategy for stripping is: + * keep first line "stack traceback:" + * part1: + * analyse all lines of the stack from bottom to top of the stack (first line to last line) + * extract the "file:line:" part of the line + * compare it with the "file:line" part of the error message + * if it does not match strip the line + * if it matches, keep the line and move to part 2 + * part2: + * anything NOT starting with luaunit.lua is the interesting part of the stack trace + * anything starting again with luaunit.lua is part of the test launcher and should be stripped out + ]] + + local function isLuaunitInternalLine( s ) + -- return true if line of stack trace comes from inside luaunit + return s:find('[/\\]luaunit%.lua:%d+: ') ~= nil + end + + -- print( '<<'..stackTrace..'>>' ) + + local t = strsplit( '\n', stackTrace ) + -- print( prettystr(t) ) + + local idx = 2 + + local errMsgFileLine = extractFileLineInfo(errMsg) + -- print('emfi="'..errMsgFileLine..'"') + + -- remove lines that are still part of luaunit + while t[idx] and extractFileLineInfo(t[idx]) ~= errMsgFileLine do + -- print('Removing : '..t[idx] ) + table.remove(t, idx) + end + + -- keep lines until we hit luaunit again + while t[idx] and (not isLuaunitInternalLine(t[idx])) do + -- print('Keeping : '..t[idx] ) + idx = idx + 1 + end + + -- remove remaining luaunit lines + while t[idx] do + -- print('Removing2 : '..t[idx] ) + table.remove(t, idx) + end + + -- print( prettystr(t) ) + return table.concat( t, '\n') + +end +M.private.stripLuaunitTrace2 = stripLuaunitTrace2 + + +local function prettystr_sub(v, indentLevel, printTableRefs, cycleDetectTable ) + local type_v = type(v) + if "string" == type_v then + -- use clever delimiters according to content: + -- enclose with single quotes if string contains ", but no ' + if v:find('"', 1, true) and not v:find("'", 1, true) then + return "'" .. v .. "'" + end + -- use double quotes otherwise, escape embedded " + return '"' .. v:gsub('"', '\\"') .. '"' + + elseif "table" == type_v then + --if v.__class__ then + -- return string.gsub( tostring(v), 'table', v.__class__ ) + --end + return M.private._table_tostring(v, indentLevel, printTableRefs, cycleDetectTable) + + elseif "number" == type_v then + -- eliminate differences in formatting between various Lua versions + if v ~= v then + return "#NaN" -- "not a number" + end + if v == math.huge then + return "#Inf" -- "infinite" + end + if v == -math.huge then + return "-#Inf" + end + if _VERSION == "Lua 5.3" then + local i = math.tointeger(v) + if i then + return tostring(i) + end + end + end + + return tostring(v) +end + +local function prettystr( v ) + --[[ Pretty string conversion, to display the full content of a variable of any type. + + * string are enclosed with " by default, or with ' if string contains a " + * tables are expanded to show their full content, with indentation in case of nested tables + ]]-- + local cycleDetectTable = {} + local s = prettystr_sub(v, 1, M.PRINT_TABLE_REF_IN_ERROR_MSG, cycleDetectTable) + if cycleDetectTable.detected and not M.PRINT_TABLE_REF_IN_ERROR_MSG then + -- some table contain recursive references, + -- so we must recompute the value by including all table references + -- else the result looks like crap + cycleDetectTable = {} + s = prettystr_sub(v, 1, true, cycleDetectTable) + end + return s +end +M.prettystr = prettystr + +function M.adjust_err_msg_with_iter( err_msg, iter_msg ) + --[[ Adjust the error message err_msg: trim the FAILURE_PREFIX or SUCCESS_PREFIX information if needed, + add the iteration message if any and return the result. + + err_msg: string, error message captured with pcall + iter_msg: a string describing the current iteration ("iteration N") or nil + if there is no iteration in this test. + + Returns: (new_err_msg, test_status) + new_err_msg: string, adjusted error message, or nil in case of success + test_status: M.NodeStatus.FAIL, SUCCESS or ERROR according to the information + contained in the error message. + ]] + if iter_msg then + iter_msg = iter_msg..', ' + else + iter_msg = '' + end + + local RE_FILE_LINE = '.*:%d+: ' + + -- error message is not necessarily a string, + -- so convert the value to string with prettystr() + if type( err_msg ) ~= 'string' then + err_msg = prettystr( err_msg ) + end + + if (err_msg:find( M.SUCCESS_PREFIX ) == 1) or err_msg:match( '('..RE_FILE_LINE..')' .. M.SUCCESS_PREFIX .. ".*" ) then + -- test finished early with success() + return nil, M.NodeStatus.SUCCESS + end + + if (err_msg:find( M.SKIP_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.SKIP_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub('.*'..M.SKIP_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.SKIP + end + + if (err_msg:find( M.FAILURE_PREFIX ) == 1) or (err_msg:match( '('..RE_FILE_LINE..')' .. M.FAILURE_PREFIX .. ".*" ) ~= nil) then + -- substitute prefix by iteration message + err_msg = err_msg:gsub(M.FAILURE_PREFIX, iter_msg, 1) + -- print("failure detected") + return err_msg, M.NodeStatus.FAIL + end + + + + -- print("error detected") + -- regular error, not a failure + if iter_msg then + local match + -- "./test\\test_luaunit.lua:2241: some error msg + match = err_msg:match( '(.*:%d+: ).*' ) + if match then + err_msg = err_msg:gsub( match, match .. iter_msg ) + else + -- no file:line: infromation, just add the iteration info at the beginning of the line + err_msg = iter_msg .. err_msg + end + end + return err_msg, M.NodeStatus.ERROR +end + +local function tryMismatchFormatting( table_a, table_b, doDeepAnalysis ) + --[[ + Prepares a nice error message when comparing tables, performing a deeper + analysis. + + Arguments: + * table_a, table_b: tables to be compared + * doDeepAnalysis: + M.DEFAULT_DEEP_ANALYSIS: (the default if not specified) perform deep analysis only for big lists and big dictionnaries + M.FORCE_DEEP_ANALYSIS : always perform deep analysis + M.DISABLE_DEEP_ANALYSIS: never perform deep analysis + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- check if table_a & table_b are suitable for deep analysis + if type(table_a) ~= 'table' or type(table_b) ~= 'table' then + return false + end + + if doDeepAnalysis == M.DISABLE_DEEP_ANALYSIS then + return false + end + + local len_a, len_b, isPureList = #table_a, #table_b, true + + for k1, v1 in pairs(table_a) do + if type(k1) ~= 'number' or k1 > len_a then + -- this table a mapping + isPureList = false + break + end + end + + if isPureList then + for k2, v2 in pairs(table_b) do + if type(k2) ~= 'number' or k2 > len_b then + -- this table a mapping + isPureList = false + break + end + end + end + + if isPureList and math.min(len_a, len_b) < M.LIST_DIFF_ANALYSIS_THRESHOLD then + if not (doDeepAnalysis == M.FORCE_DEEP_ANALYSIS) then + return false + end + end + + if isPureList then + return M.private.mismatchFormattingPureList( table_a, table_b ) + else + -- only work on mapping for the moment + -- return M.private.mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + return false + end +end +M.private.tryMismatchFormatting = tryMismatchFormatting + +local function getTaTbDescr() + if not M.ORDER_ACTUAL_EXPECTED then + return 'expected', 'actual' + end + return 'actual', 'expected' +end + +local function extendWithStrFmt( res, ... ) + table.insert( res, string.format( ... ) ) +end + +local function mismatchFormattingMapping( table_a, table_b, doDeepAnalysis ) + --[[ + Prepares a nice error message when comparing tables which are not pure lists, performing a deeper + analysis. + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + + -- disable for the moment + --[[ + local result = {} + local descrTa, descrTb = getTaTbDescr() + + local keysCommon = {} + local keysOnlyTa = {} + local keysOnlyTb = {} + local keysDiffTaTb = {} + + local k, v + + for k,v in pairs( table_a ) do + if is_equal( v, table_b[k] ) then + table.insert( keysCommon, k ) + else + if table_b[k] == nil then + table.insert( keysOnlyTa, k ) + else + table.insert( keysDiffTaTb, k ) + end + end + end + + for k,v in pairs( table_b ) do + if not is_equal( v, table_a[k] ) and table_a[k] == nil then + table.insert( keysOnlyTb, k ) + end + end + + local len_a = #keysCommon + #keysDiffTaTb + #keysOnlyTa + local len_b = #keysCommon + #keysDiffTaTb + #keysOnlyTb + local limited_display = (len_a < 5 or len_b < 5) + + if math.min(len_a, len_b) < M.TABLE_DIFF_ANALYSIS_THRESHOLD then + return false + end + + if not limited_display then + if len_a == len_b then + extendWithStrFmt( result, 'Table A (%s) and B (%s) both have %d items', descrTa, descrTb, len_a ) + else + extendWithStrFmt( result, 'Table A (%s) has %d items and table B (%s) has %d items', descrTa, len_a, descrTb, len_b ) + end + + if #keysCommon == 0 and #keysDiffTaTb == 0 then + table.insert( result, 'Table A and B have no keys in common, they are totally different') + else + local s_other = 'other ' + if #keysCommon then + extendWithStrFmt( result, 'Table A and B have %d identical items', #keysCommon ) + else + table.insert( result, 'Table A and B have no identical items' ) + s_other = '' + end + + if #keysDiffTaTb ~= 0 then + result[#result] = string.format( '%s and %d items differing present in both tables', result[#result], #keysDiffTaTb) + else + result[#result] = string.format( '%s and no %sitems differing present in both tables', result[#result], s_other, #keysDiffTaTb) + end + end + + extendWithStrFmt( result, 'Table A has %d keys not present in table B and table B has %d keys not present in table A', #keysOnlyTa, #keysOnlyTb ) + end + + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr(k) + end + + if #keysDiffTaTb ~= 0 then + table.insert( result, 'Items differing in A and B:') + for k,v in sortedPairs( keysDiffTaTb ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysOnlyTa ~= 0 then + table.insert( result, 'Items only in table A:' ) + for k,v in sortedPairs( keysOnlyTa ) do + extendWithStrFmt( result, ' - A[%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + if #keysOnlyTb ~= 0 then + table.insert( result, 'Items only in table B:' ) + for k,v in sortedPairs( keysOnlyTb ) do + extendWithStrFmt( result, ' + B[%s]: %s', keytostring(v), prettystr(table_b[v]) ) + end + end + + if #keysCommon ~= 0 then + table.insert( result, 'Items common to A and B:') + for k,v in sortedPairs( keysCommon ) do + extendWithStrFmt( result, ' = A and B [%s]: %s', keytostring(v), prettystr(table_a[v]) ) + end + end + + return true, table.concat( result, '\n') + ]] +end +M.private.mismatchFormattingMapping = mismatchFormattingMapping + +local function mismatchFormattingPureList( table_a, table_b ) + --[[ + Prepares a nice error message when comparing tables which are lists, performing a deeper + analysis. + + Returns: {success, result} + * success: false if deep analysis could not be performed + in this case, just use standard assertion message + * result: if success is true, a multi-line string with deep analysis of the two lists + ]] + local result, descrTa, descrTb = {}, getTaTbDescr() + + local len_a, len_b, refa, refb = #table_a, #table_b, '', '' + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + refa, refb = string.format( '<%s> ', M.private.table_ref(table_a)), string.format('<%s> ', M.private.table_ref(table_b) ) + end + local longest, shortest = math.max(len_a, len_b), math.min(len_a, len_b) + local deltalv = longest - shortest + + local commonUntil = shortest + for i = 1, shortest do + if not is_equal(table_a[i], table_b[i]) then + commonUntil = i - 1 + break + end + end + + local commonBackTo = shortest - 1 + for i = 0, shortest - 1 do + if not is_equal(table_a[len_a-i], table_b[len_b-i]) then + commonBackTo = i - 1 + break + end + end + + + table.insert( result, 'List difference analysis:' ) + if len_a == len_b then + -- TODO: handle expected/actual naming + extendWithStrFmt( result, '* lists %sA (%s) and %sB (%s) have the same size', refa, descrTa, refb, descrTb ) + else + extendWithStrFmt( result, '* list sizes differ: list %sA (%s) has %d items, list %sB (%s) has %d items', refa, descrTa, len_a, refb, descrTb, len_b ) + end + + extendWithStrFmt( result, '* lists A and B start differing at index %d', commonUntil+1 ) + if commonBackTo >= 0 then + if deltalv > 0 then + extendWithStrFmt( result, '* lists A and B are equal again from index %d for A, %d for B', len_a-commonBackTo, len_b-commonBackTo ) + else + extendWithStrFmt( result, '* lists A and B are equal again from index %d', len_a-commonBackTo ) + end + end + + local function insertABValue(ai, bi) + bi = bi or ai + if is_equal( table_a[ai], table_b[bi]) then + return extendWithStrFmt( result, ' = A[%d], B[%d]: %s', ai, bi, prettystr(table_a[ai]) ) + else + extendWithStrFmt( result, ' - A[%d]: %s', ai, prettystr(table_a[ai])) + extendWithStrFmt( result, ' + B[%d]: %s', bi, prettystr(table_b[bi])) + end + end + + -- common parts to list A & B, at the beginning + if commonUntil > 0 then + table.insert( result, '* Common parts:' ) + for i = 1, commonUntil do + insertABValue( i ) + end + end + + -- diffing parts to list A & B + if commonUntil < shortest - commonBackTo - 1 then + table.insert( result, '* Differing parts:' ) + for i = commonUntil + 1, shortest - commonBackTo - 1 do + insertABValue( i ) + end + end + + -- display indexes of one list, with no match on other list + if shortest - commonBackTo <= longest - commonBackTo - 1 then + table.insert( result, '* Present only in one list:' ) + for i = shortest - commonBackTo, longest - commonBackTo - 1 do + if len_a > len_b then + extendWithStrFmt( result, ' - A[%d]: %s', i, prettystr(table_a[i]) ) + -- table.insert( result, '+ (no matching B index)') + else + -- table.insert( result, '- no matching A index') + extendWithStrFmt( result, ' + B[%d]: %s', i, prettystr(table_b[i]) ) + end + end + end + + -- common parts to list A & B, at the end + if commonBackTo >= 0 then + table.insert( result, '* Common parts at the end of the lists' ) + for i = longest - commonBackTo, longest do + if len_a > len_b then + insertABValue( i, i-deltalv ) + else + insertABValue( i-deltalv, i ) + end + end + end + + return true, table.concat( result, '\n') +end +M.private.mismatchFormattingPureList = mismatchFormattingPureList + +local function prettystrPairs(value1, value2, suffix_a, suffix_b) + --[[ + This function helps with the recurring task of constructing the "expected + vs. actual" error messages. It takes two arbitrary values and formats + corresponding strings with prettystr(). + + To keep the (possibly complex) output more readable in case the resulting + strings contain line breaks, they get automatically prefixed with additional + newlines. Both suffixes are optional (default to empty strings), and get + appended to the "value1" string. "suffix_a" is used if line breaks were + encountered, "suffix_b" otherwise. + + Returns the two formatted strings (including padding/newlines). + ]] + local str1, str2 = prettystr(value1), prettystr(value2) + if hasNewLine(str1) or hasNewLine(str2) then + -- line break(s) detected, add padding + return "\n" .. str1 .. (suffix_a or ""), "\n" .. str2 + end + return str1 .. (suffix_b or ""), str2 +end +M.private.prettystrPairs = prettystrPairs + +local UNKNOWN_REF = 'table 00-unknown ref' +local ref_generator = { value=1, [UNKNOWN_REF]=0 } + +local function table_ref( t ) + -- return the default tostring() for tables, with the table ID, even if the table has a metatable + -- with the __tostring converter + local ref = '' + local mt = getmetatable( t ) + if mt == nil then + ref = tostring(t) + else + local success, result + success, result = pcall(setmetatable, t, nil) + if not success then + -- protected table, if __tostring is defined, we can + -- not get the reference. And we can not know in advance. + ref = tostring(t) + if not ref:match( 'table: 0?x?[%x]+' ) then + return UNKNOWN_REF + end + else + ref = tostring(t) + setmetatable( t, mt ) + end + end + -- strip the "table: " part + ref = ref:sub(8) + if ref ~= UNKNOWN_REF and ref_generator[ref] == nil then + -- Create a new reference number + ref_generator[ref] = ref_generator.value + ref_generator.value = ref_generator.value+1 + end + if M.PRINT_TABLE_REF_IN_ERROR_MSG then + return string.format('table %02d-%s', ref_generator[ref], ref) + else + return string.format('table %02d', ref_generator[ref]) + end +end +M.private.table_ref = table_ref + +local TABLE_TOSTRING_SEP = ", " +local TABLE_TOSTRING_SEP_LEN = string.len(TABLE_TOSTRING_SEP) + +local function _table_tostring( tbl, indentLevel, printTableRefs, cycleDetectTable ) + printTableRefs = printTableRefs or M.PRINT_TABLE_REF_IN_ERROR_MSG + cycleDetectTable = cycleDetectTable or {} + cycleDetectTable[tbl] = true + + local result, dispOnMultLines = {}, false + + -- like prettystr but do not enclose with "" if the string is just alphanumerical + -- this is better for displaying table keys who are often simple strings + local function keytostring(k) + if "string" == type(k) and k:match("^[_%a][_%w]*$") then + return k + end + return prettystr_sub(k, indentLevel+1, printTableRefs, cycleDetectTable) + end + + local mt = getmetatable( tbl ) + + if mt and mt.__tostring then + -- if table has a __tostring() function in its metatable, use it to display the table + -- else, compute a regular table + result = tostring(tbl) + if type(result) ~= 'string' then + return string.format( '', prettystr(result) ) + end + result = strsplit( '\n', result ) + return M.private._table_tostring_format_multiline_string( result, indentLevel ) + + else + -- no metatable, compute the table representation + + local entry, count, seq_index = nil, 0, 1 + for k, v in sortedPairs( tbl ) do + + -- key part + if k == seq_index then + -- for the sequential part of tables, we'll skip the "=" output + entry = '' + seq_index = seq_index + 1 + elseif cycleDetectTable[k] then + -- recursion in the key detected + cycleDetectTable.detected = true + entry = "<"..table_ref(k)..">=" + else + entry = keytostring(k) .. "=" + end + + -- value part + if cycleDetectTable[v] then + -- recursion in the value detected! + cycleDetectTable.detected = true + entry = entry .. "<"..table_ref(v)..">" + else + entry = entry .. + prettystr_sub( v, indentLevel+1, printTableRefs, cycleDetectTable ) + end + count = count + 1 + result[count] = entry + end + return M.private._table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + end + +end +M.private._table_tostring = _table_tostring -- prettystr_sub() needs it + +local function _table_tostring_format_multiline_string( tbl_str, indentLevel ) + local indentString = '\n'..string.rep(" ", indentLevel - 1) + return table.concat( tbl_str, indentString ) + +end +M.private._table_tostring_format_multiline_string = _table_tostring_format_multiline_string + + +local function _table_tostring_format_result( tbl, result, indentLevel, printTableRefs ) + -- final function called in _table_to_string() to format the resulting list of + -- string describing the table. + + local dispOnMultLines = false + + -- set dispOnMultLines to true if the maximum LINE_LENGTH would be exceeded with the values + local totalLength = 0 + for k, v in ipairs( result ) do + totalLength = totalLength + string.len( v ) + if totalLength >= M.LINE_LENGTH then + dispOnMultLines = true + break + end + end + + -- set dispOnMultLines to true if the max LINE_LENGTH would be exceeded + -- with the values and the separators. + if not dispOnMultLines then + -- adjust with length of separator(s): + -- two items need 1 sep, three items two seps, ... plus len of '{}' + if #result > 0 then + totalLength = totalLength + TABLE_TOSTRING_SEP_LEN * (#result - 1) + end + dispOnMultLines = (totalLength + 2 >= M.LINE_LENGTH) + end + + -- now reformat the result table (currently holding element strings) + if dispOnMultLines then + local indentString = string.rep(" ", indentLevel - 1) + result = { + "{\n ", + indentString, + table.concat(result, ",\n " .. indentString), + "\n", + indentString, + "}" + } + else + result = {"{", table.concat(result, TABLE_TOSTRING_SEP), "}"} + end + if printTableRefs then + table.insert(result, 1, "<"..table_ref(tbl).."> ") -- prepend table ref + end + return table.concat(result) +end +M.private._table_tostring_format_result = _table_tostring_format_result -- prettystr_sub() needs it + +local function table_findkeyof(t, element) + -- Return the key k of the given element in table t, so that t[k] == element + -- (or `nil` if element is not present within t). Note that we use our + -- 'general' is_equal comparison for matching, so this function should + -- handle table-type elements gracefully and consistently. + if type(t) == "table" then + for k, v in pairs(t) do + if is_equal(v, element) then + return k + end + end + end + return nil +end + +local function _is_table_items_equals(actual, expected ) + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false + + elseif (type_a == 'table') --[[and (type_e == 'table')]] then + for k, v in pairs(actual) do + if table_findkeyof(expected, v) == nil then + return false -- v not contained in expected + end + end + for k, v in pairs(expected) do + if table_findkeyof(actual, v) == nil then + return false -- v not contained in actual + end + end + return true + + elseif actual ~= expected then + return false + end + + return true +end + +--[[ +This is a specialized metatable to help with the bookkeeping of recursions +in _is_table_equals(). It provides an __index table that implements utility +functions for easier management of the table. The "cached" method queries +the state of a specific (actual,expected) pair; and the "store" method sets +this state to the given value. The state of pairs not "seen" / visited is +assumed to be `nil`. +]] +local _recursion_cache_MT = { + __index = { + -- Return the cached value for an (actual,expected) pair (or `nil`) + cached = function(t, actual, expected) + local subtable = t[actual] or {} + return subtable[expected] + end, + + -- Store cached value for a specific (actual,expected) pair. + -- Returns the value, so it's easy to use for a "tailcall" (return ...). + store = function(t, actual, expected, value, asymmetric) + local subtable = t[actual] + if not subtable then + subtable = {} + t[actual] = subtable + end + subtable[expected] = value + + -- Unless explicitly marked "asymmetric": Consider the recursion + -- on (expected,actual) to be equivalent to (actual,expected) by + -- default, and thus cache the value for both. + if not asymmetric then + t:store(expected, actual, value, true) + end + + return value + end + } +} + +local function _is_table_equals(actual, expected, cycleDetectTable) + local type_a, type_e = type(actual), type(expected) + + if type_a ~= type_e then + return false -- different types won't match + end + + if type_a ~= 'table' then + -- other typtes compare directly + return actual == expected + end + + -- print('_is_table_equals( \n '..prettystr(actual)..'\n , '..prettystr(expected)..'\n , '..prettystr(recursions)..' \n )') + + cycleDetectTable = cycleDetectTable or { actual={}, expected={} } + if cycleDetectTable.actual[ actual ] then + -- oh, we hit a cycle in actual + if cycleDetectTable.expected[ expected ] then + -- uh, we hit a cycle at the same time in expected + -- so the two tables have similar structure + return true + end + + -- cycle was hit only in actual, the structure differs from expected + return false + end + + if cycleDetectTable.expected[ expected ] then + -- no cycle in actual, but cycle in expected + -- the structure differ + return false + end + + -- at this point, no table cycle detected, we are + -- seeing this table for the first time + + -- mark the cycle detection + cycleDetectTable.actual[ actual ] = true + cycleDetectTable.expected[ expected ] = true + + + local actualKeysMatched = {} + for k, v in pairs(actual) do + actualKeysMatched[k] = true -- Keep track of matched keys + if not _is_table_equals(v, expected[k], cycleDetectTable) then + -- table differs on this key + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + end + + for k, v in pairs(expected) do + if not actualKeysMatched[k] then + -- Found a key that we did not see in "actual" -> mismatch + -- clear the cycle detection before returning + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return false + end + -- Otherwise actual[k] was already matched against v = expected[k]. + end + + -- all key match, we have a match ! + cycleDetectTable.actual[ actual ] = nil + cycleDetectTable.expected[ expected ] = nil + return true +end +M.private._is_table_equals = _is_table_equals +is_equal = _is_table_equals + +local function failure(main_msg, extra_msg_or_nil, level) + -- raise an error indicating a test failure + -- for error() compatibility we adjust "level" here (by +1), to report the + -- calling context + local msg + if type(extra_msg_or_nil) == 'string' and extra_msg_or_nil:len() > 0 then + msg = extra_msg_or_nil .. '\n' .. main_msg + else + msg = main_msg + end + error(M.FAILURE_PREFIX .. msg, (level or 1) + 1) +end + +local function fail_fmt(level, extra_msg_or_nil, ...) + -- failure with printf-style formatted message and given error level + failure(string.format(...), extra_msg_or_nil, (level or 1) + 1) +end +M.private.fail_fmt = fail_fmt + +local function error_fmt(level, ...) + -- printf-style error() + error(string.format(...), (level or 1) + 1) +end + +---------------------------------------------------------------- +-- +-- assertions +-- +---------------------------------------------------------------- + +local function errorMsgEquality(actual, expected, doDeepAnalysis) + + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + if type(expected) == 'string' or type(expected) == 'table' then + local strExpected, strActual = prettystrPairs(expected, actual) + local result = string.format("expected: %s\nactual: %s", strExpected, strActual) + + -- extend with mismatch analysis if possible: + local success, mismatchResult + success, mismatchResult = tryMismatchFormatting( actual, expected, doDeepAnalysis ) + if success then + result = table.concat( { result, mismatchResult }, '\n' ) + end + return result + end + return string.format("expected: %s, actual: %s", + prettystr(expected), prettystr(actual)) +end + +function M.assertError(f, ...) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + if pcall( f, ... ) then + failure( "Expected an error when calling function but no error generated", nil, 2 ) + end +end + +function M.fail( msg ) + -- stops a test due to a failure + failure( msg, nil, 2 ) +end + +function M.failIf( cond, msg ) + -- Fails a test with "msg" if condition is true + if cond then + failure( msg, nil, 2 ) + end +end + +function M.skip(msg) + -- skip a running test + error_fmt(2, M.SKIP_PREFIX .. msg) +end + +function M.skipIf( cond, msg ) + -- skip a running test if condition is met + if cond then + error_fmt(2, M.SKIP_PREFIX .. msg) + end +end + +function M.runOnlyIf( cond, msg ) + -- continue a running test if condition is met, else skip it + if not cond then + error_fmt(2, M.SKIP_PREFIX .. prettystr(msg)) + end +end + +function M.success() + -- stops a test with a success + error_fmt(2, M.SUCCESS_PREFIX) +end + +function M.successIf( cond ) + -- stops a test with a success if condition is met + if cond then + error_fmt(2, M.SUCCESS_PREFIX) + end +end + + +------------------------------------------------------------------ +-- Equality assertions +------------------------------------------------------------------ + +function M.assertEquals(actual, expected, extra_msg_or_nil, doDeepAnalysis) + if type(actual) == 'table' and type(expected) == 'table' then + if not _is_table_equals(actual, expected) then + failure( errorMsgEquality(actual, expected, doDeepAnalysis), extra_msg_or_nil, 2 ) + end + elseif type(actual) ~= type(expected) then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + elseif actual ~= expected then + failure( errorMsgEquality(actual, expected), extra_msg_or_nil, 2 ) + end +end + +function M.almostEquals( actual, expected, margin ) + if type(actual) ~= 'number' or type(expected) ~= 'number' or type(margin) ~= 'number' then + error_fmt(3, 'almostEquals: must supply only number arguments.\nArguments supplied: %s, %s, %s', + prettystr(actual), prettystr(expected), prettystr(margin)) + end + if margin < 0 then + error_fmt(3, 'almostEquals: margin must not be negative, current value is ' .. margin) + end + return math.abs(expected - actual) <= margin +end + +function M.assertAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are close by margin + margin = margin or M.EPS + if not M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are not almost equal\n' .. + 'Actual: %s, expected: %s, delta %s above margin of %s', + actual, expected, delta, margin) + end +end + +function M.assertNotEquals(actual, expected, extra_msg_or_nil) + if type(actual) ~= type(expected) then + return + end + + if type(actual) == 'table' and type(expected) == 'table' then + if not _is_table_equals(actual, expected) then + return + end + elseif actual ~= expected then + return + end + fail_fmt(2, extra_msg_or_nil, 'Received the not expected value: %s', prettystr(actual)) +end + +function M.assertNotAlmostEquals( actual, expected, margin, extra_msg_or_nil ) + -- check that two floats are not close by margin + margin = margin or M.EPS + if M.almostEquals(actual, expected, margin) then + if not M.ORDER_ACTUAL_EXPECTED then + expected, actual = actual, expected + end + local delta = math.abs(actual - expected) + fail_fmt(2, extra_msg_or_nil, 'Values are almost equal\nActual: %s, expected: %s' .. + ', delta %s below margin of %s', + actual, expected, delta, margin) + end +end + +function M.assertItemsEquals(actual, expected, extra_msg_or_nil) + -- checks that the items of table expected + -- are contained in table actual. Warning, this function + -- is at least O(n^2) + if not _is_table_items_equals(actual, expected ) then + expected, actual = prettystrPairs(expected, actual) + fail_fmt(2, extra_msg_or_nil, 'Content of the tables are not identical:\nExpected: %s\nActual: %s', + expected, actual) + end +end + +------------------------------------------------------------------ +-- String assertion +------------------------------------------------------------------ + +function M.assertStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + -- assert( type(str) == 'string', 'Argument 1 of assertStrContains() should be a string.' ) ) + -- assert( type(sub) == 'string', 'Argument 2 of assertStrContains() should be a string.' ) ) + if not string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if not string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not find (case insensitively) substring %s in string %s', + sub, str) + end +end + +function M.assertNotStrContains( str, sub, isPattern, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str, sub, 1, not isPattern) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found the not expected %s %s in string %s', + isPattern and 'pattern' or 'substring', sub, str) + end +end + +function M.assertNotStrIContains( str, sub, extra_msg_or_nil ) + -- this relies on lua string.find function + -- a string always contains the empty string + if string.find(str:lower(), sub:lower(), 1, true) then + sub, str = prettystrPairs(sub, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Found (case insensitively) the not expected substring %s in string %s', + sub, str) + end +end + +function M.assertStrMatches( str, pattern, start, final, extra_msg_or_nil ) + -- Verify a full match for the string + if not strMatch( str, pattern, start, final ) then + pattern, str = prettystrPairs(pattern, str, '\n') + fail_fmt(2, extra_msg_or_nil, 'Could not match pattern %s with string %s', + pattern, str) + end +end + +local function _assertErrorMsgEquals( stripFileAndLine, expectedMsg, func, ... ) + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error: '..M.prettystr(expectedMsg), nil, 3 ) + end + if type(expectedMsg) == "string" and type(error_msg) ~= "string" then + -- table are converted to string automatically + error_msg = tostring(error_msg) + end + local differ = false + if stripFileAndLine then + if error_msg:gsub("^.+:%d+: ", "") ~= expectedMsg then + differ = true + end + else + if error_msg ~= expectedMsg then + local tr = type(error_msg) + local te = type(expectedMsg) + if te == 'table' then + if tr ~= 'table' then + differ = true + else + local ok = pcall(M.assertItemsEquals, error_msg, expectedMsg) + if not ok then + differ = true + end + end + else + differ = true + end + end + end + + if differ then + error_msg, expectedMsg = prettystrPairs(error_msg, expectedMsg) + fail_fmt(3, nil, 'Error message expected: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +function M.assertErrorMsgEquals( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + _assertErrorMsgEquals(false, expectedMsg, func, ...) +end + +function M.assertErrorMsgContentEquals(expectedMsg, func, ...) + _assertErrorMsgEquals(true, expectedMsg, func, ...) +end + +function M.assertErrorMsgContains( partialMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error containing: '..prettystr(partialMsg), nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not string.find( error_msg, partialMsg, nil, true ) then + error_msg, partialMsg = prettystrPairs(error_msg, partialMsg) + fail_fmt(2, nil, 'Error message does not contain: %s\nError message received: %s\n', + partialMsg, error_msg) + end +end + +function M.assertErrorMsgMatches( expectedMsg, func, ... ) + -- assert that calling f with the arguments will raise an error + -- example: assertError( f, 1, 2 ) => f(1,2) should generate an error + local no_error, error_msg = pcall( func, ... ) + if no_error then + failure( 'No error generated when calling function but expected error matching: "'..expectedMsg..'"', nil, 2 ) + end + if type(error_msg) ~= "string" then + error_msg = tostring(error_msg) + end + if not strMatch( error_msg, expectedMsg ) then + expectedMsg, error_msg = prettystrPairs(expectedMsg, error_msg) + fail_fmt(2, nil, 'Error message does not match pattern: %s\nError message received: %s\n', + expectedMsg, error_msg) + end +end + +------------------------------------------------------------------ +-- Type assertions +------------------------------------------------------------------ + +function M.assertEvalToTrue(value, extra_msg_or_nil) + if not value then + failure("expected: a value evaluating to true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertEvalToFalse(value, extra_msg_or_nil) + if value then + failure("expected: false or nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsTrue(value, extra_msg_or_nil) + if value ~= true then + failure("expected: true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsTrue(value, extra_msg_or_nil) + if value == true then + failure("expected: not true, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsFalse(value, extra_msg_or_nil) + if value ~= false then + failure("expected: false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsFalse(value, extra_msg_or_nil) + if value == false then + failure("expected: not false, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsNil(value, extra_msg_or_nil) + if value ~= nil then + failure("expected: nil, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNil(value, extra_msg_or_nil) + if value == nil then + failure("expected: not nil, actual: nil", extra_msg_or_nil, 2) + end +end + +--[[ +Add type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type matches the +expected string (derived from the function name): + +M.assertIsXxx(value) -> ensure that type(value) conforms to "xxx" +]] +for _, funcName in ipairs( + {'assertIsNumber', 'assertIsString', 'assertIsTable', 'assertIsBoolean', + 'assertIsFunction', 'assertIsUserdata', 'assertIsThread'} +) do + local typeExpected = funcName:match("^assertIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeExpected = typeExpected and typeExpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) ~= typeExpected then + if type(value) == 'nil' then + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: nil', + typeExpected, type(value), prettystrPairs(value)) + else + fail_fmt(2, extra_msg_or_nil, 'expected: a %s value, actual: type %s, value %s', + typeExpected, type(value), prettystrPairs(value)) + end + end + end +end + +--[[ +Add shortcuts for verifying type of a variable, without failure (luaunit v2 compatibility) +M.isXxx(value) -> returns true if type(value) conforms to "xxx" +]] +for _, typeExpected in ipairs( + {'Number', 'String', 'Table', 'Boolean', + 'Function', 'Userdata', 'Thread', 'Nil' } +) do + local typeExpectedLower = typeExpected:lower() + local isType = function(value) + return (type(value) == typeExpectedLower) + end + M['is'..typeExpected] = isType + M['is_'..typeExpectedLower] = isType +end + +--[[ +Add non-type assertion functions to the module table M. Each of these functions +takes a single parameter "value", and checks that its Lua type differs from the +expected string (derived from the function name): + +M.assertNotIsXxx(value) -> ensure that type(value) is not "xxx" +]] +for _, funcName in ipairs( + {'assertNotIsNumber', 'assertNotIsString', 'assertNotIsTable', 'assertNotIsBoolean', + 'assertNotIsFunction', 'assertNotIsUserdata', 'assertNotIsThread'} +) do + local typeUnexpected = funcName:match("^assertNotIs([A-Z]%a*)$") + -- Lua type() always returns lowercase, also make sure the match() succeeded + typeUnexpected = typeUnexpected and typeUnexpected:lower() + or error("bad function name '"..funcName.."' for type assertion") + + M[funcName] = function(value, extra_msg_or_nil) + if type(value) == typeUnexpected then + fail_fmt(2, extra_msg_or_nil, 'expected: not a %s type, actual: value %s', + typeUnexpected, prettystrPairs(value)) + end + end +end + +function M.assertIs(actual, expected, extra_msg_or_nil) + if actual ~= expected then + if not M.ORDER_ACTUAL_EXPECTED then + actual, expected = expected, actual + end + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + expected, actual = prettystrPairs(expected, actual, '\n', '') + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should not be different\nExpected: %s\nReceived: %s', + expected, actual) + end +end + +function M.assertNotIs(actual, expected, extra_msg_or_nil) + if actual == expected then + local old_print_table_ref_in_error_msg = M.PRINT_TABLE_REF_IN_ERROR_MSG + M.PRINT_TABLE_REF_IN_ERROR_MSG = true + local s_expected + if not M.ORDER_ACTUAL_EXPECTED then + s_expected = prettystrPairs(actual) + else + s_expected = prettystrPairs(expected) + end + M.PRINT_TABLE_REF_IN_ERROR_MSG = old_print_table_ref_in_error_msg + fail_fmt(2, extra_msg_or_nil, 'expected and actual object should be different: %s', s_expected ) + end +end + + +------------------------------------------------------------------ +-- Scientific assertions +------------------------------------------------------------------ + + +function M.assertIsNaN(value, extra_msg_or_nil) + if type(value) ~= "number" or value == value then + failure("expected: NaN, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsNaN(value, extra_msg_or_nil) + if type(value) == "number" and value ~= value then + failure("expected: not NaN, actual: NaN", extra_msg_or_nil, 2) + end +end + +function M.assertIsInf(value, extra_msg_or_nil) + if type(value) ~= "number" or math.abs(value) ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= math.huge then + failure("expected: #Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsMinusInf(value, extra_msg_or_nil) + if type(value) ~= "number" or value ~= -math.huge then + failure("expected: -#Inf, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertNotIsPlusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == math.huge then + failure("expected: not #Inf, actual: #Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusInf(value, extra_msg_or_nil) + if type(value) == "number" and value == -math.huge then + failure("expected: not -#Inf, actual: -#Inf", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsInf(value, extra_msg_or_nil) + if type(value) == "number" and math.abs(value) == math.huge then + failure("expected: not infinity, actual: " .. prettystr(value), extra_msg_or_nil, 2) + end +end + +function M.assertIsPlusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == -math.huge) then + -- more precise error diagnosis + failure("expected: +0.0, actual: -0.0", extra_msg_or_nil, 2) + else if (1/value ~= math.huge) then + -- strange, case should have already been covered + failure("expected: +0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertIsMinusZero(value, extra_msg_or_nil) + if type(value) ~= 'number' or value ~= 0 then + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + else if (1/value == math.huge) then + -- more precise error diagnosis + failure("expected: -0.0, actual: +0.0", extra_msg_or_nil, 2) + else if (1/value ~= -math.huge) then + -- strange, case should have already been covered + failure("expected: -0.0, actual: " ..prettystr(value), extra_msg_or_nil, 2) + end + end + end +end + +function M.assertNotIsPlusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == math.huge) then + failure("expected: not +0.0, actual: +0.0", extra_msg_or_nil, 2) + end +end + +function M.assertNotIsMinusZero(value, extra_msg_or_nil) + if type(value) == 'number' and (1/value == -math.huge) then + failure("expected: not -0.0, actual: -0.0", extra_msg_or_nil, 2) + end +end + +function M.assertTableContains(t, expected) + -- checks that table t contains the expected element + if table_findkeyof(t, expected) == nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, 'Table %s does NOT contain the expected element %s', + t, expected) + end +end + +function M.assertNotTableContains(t, expected) + -- checks that table t doesn't contain the expected element + local k = table_findkeyof(t, expected) + if k ~= nil then + t, expected = prettystrPairs(t, expected) + fail_fmt(2, 'Table %s DOES contain the unwanted element %s (at key %s)', + t, expected, prettystr(k)) + end +end + +---------------------------------------------------------------- +-- Compatibility layer +---------------------------------------------------------------- + +-- for compatibility with LuaUnit v2.x +function M.wrapFunctions() + -- In LuaUnit version <= 2.1 , this function was necessary to include + -- a test function inside the global test suite. Nowadays, the functions + -- are simply run directly as part of the test discovery process. + -- so just do nothing ! + io.stderr:write[[Use of WrapFunctions() is no longer needed. +Just prefix your test function names with "test" or "Test" and they +will be picked up and run by LuaUnit. +]] +end + +local list_of_funcs = { + -- { official function name , alias } + + -- general assertions + { 'assertEquals' , 'assert_equals' }, + { 'assertItemsEquals' , 'assert_items_equals' }, + { 'assertNotEquals' , 'assert_not_equals' }, + { 'assertAlmostEquals' , 'assert_almost_equals' }, + { 'assertNotAlmostEquals' , 'assert_not_almost_equals' }, + { 'assertEvalToTrue' , 'assert_eval_to_true' }, + { 'assertEvalToFalse' , 'assert_eval_to_false' }, + { 'assertStrContains' , 'assert_str_contains' }, + { 'assertStrIContains' , 'assert_str_icontains' }, + { 'assertNotStrContains' , 'assert_not_str_contains' }, + { 'assertNotStrIContains' , 'assert_not_str_icontains' }, + { 'assertStrMatches' , 'assert_str_matches' }, + { 'assertError' , 'assert_error' }, + { 'assertErrorMsgEquals' , 'assert_error_msg_equals' }, + { 'assertErrorMsgContains' , 'assert_error_msg_contains' }, + { 'assertErrorMsgMatches' , 'assert_error_msg_matches' }, + { 'assertErrorMsgContentEquals', 'assert_error_msg_content_equals' }, + { 'assertIs' , 'assert_is' }, + { 'assertNotIs' , 'assert_not_is' }, + { 'assertTableContains' , 'assert_table_contains' }, + { 'assertNotTableContains' , 'assert_not_table_contains' }, + { 'wrapFunctions' , 'WrapFunctions' }, + { 'wrapFunctions' , 'wrap_functions' }, + + -- type assertions: assertIsXXX -> assert_is_xxx + { 'assertIsNumber' , 'assert_is_number' }, + { 'assertIsString' , 'assert_is_string' }, + { 'assertIsTable' , 'assert_is_table' }, + { 'assertIsBoolean' , 'assert_is_boolean' }, + { 'assertIsNil' , 'assert_is_nil' }, + { 'assertIsTrue' , 'assert_is_true' }, + { 'assertIsFalse' , 'assert_is_false' }, + { 'assertIsNaN' , 'assert_is_nan' }, + { 'assertIsInf' , 'assert_is_inf' }, + { 'assertIsPlusInf' , 'assert_is_plus_inf' }, + { 'assertIsMinusInf' , 'assert_is_minus_inf' }, + { 'assertIsPlusZero' , 'assert_is_plus_zero' }, + { 'assertIsMinusZero' , 'assert_is_minus_zero' }, + { 'assertIsFunction' , 'assert_is_function' }, + { 'assertIsThread' , 'assert_is_thread' }, + { 'assertIsUserdata' , 'assert_is_userdata' }, + + -- type assertions: assertIsXXX -> assertXxx + { 'assertIsNumber' , 'assertNumber' }, + { 'assertIsString' , 'assertString' }, + { 'assertIsTable' , 'assertTable' }, + { 'assertIsBoolean' , 'assertBoolean' }, + { 'assertIsNil' , 'assertNil' }, + { 'assertIsTrue' , 'assertTrue' }, + { 'assertIsFalse' , 'assertFalse' }, + { 'assertIsNaN' , 'assertNaN' }, + { 'assertIsInf' , 'assertInf' }, + { 'assertIsPlusInf' , 'assertPlusInf' }, + { 'assertIsMinusInf' , 'assertMinusInf' }, + { 'assertIsPlusZero' , 'assertPlusZero' }, + { 'assertIsMinusZero' , 'assertMinusZero'}, + { 'assertIsFunction' , 'assertFunction' }, + { 'assertIsThread' , 'assertThread' }, + { 'assertIsUserdata' , 'assertUserdata' }, + + -- type assertions: assertIsXXX -> assert_xxx (luaunit v2 compat) + { 'assertIsNumber' , 'assert_number' }, + { 'assertIsString' , 'assert_string' }, + { 'assertIsTable' , 'assert_table' }, + { 'assertIsBoolean' , 'assert_boolean' }, + { 'assertIsNil' , 'assert_nil' }, + { 'assertIsTrue' , 'assert_true' }, + { 'assertIsFalse' , 'assert_false' }, + { 'assertIsNaN' , 'assert_nan' }, + { 'assertIsInf' , 'assert_inf' }, + { 'assertIsPlusInf' , 'assert_plus_inf' }, + { 'assertIsMinusInf' , 'assert_minus_inf' }, + { 'assertIsPlusZero' , 'assert_plus_zero' }, + { 'assertIsMinusZero' , 'assert_minus_zero' }, + { 'assertIsFunction' , 'assert_function' }, + { 'assertIsThread' , 'assert_thread' }, + { 'assertIsUserdata' , 'assert_userdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_is_xxx + { 'assertNotIsNumber' , 'assert_not_is_number' }, + { 'assertNotIsString' , 'assert_not_is_string' }, + { 'assertNotIsTable' , 'assert_not_is_table' }, + { 'assertNotIsBoolean' , 'assert_not_is_boolean' }, + { 'assertNotIsNil' , 'assert_not_is_nil' }, + { 'assertNotIsTrue' , 'assert_not_is_true' }, + { 'assertNotIsFalse' , 'assert_not_is_false' }, + { 'assertNotIsNaN' , 'assert_not_is_nan' }, + { 'assertNotIsInf' , 'assert_not_is_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_is_function' }, + { 'assertNotIsThread' , 'assert_not_is_thread' }, + { 'assertNotIsUserdata' , 'assert_not_is_userdata' }, + + -- type assertions: assertNotIsXXX -> assertNotXxx (luaunit v2 compat) + { 'assertNotIsNumber' , 'assertNotNumber' }, + { 'assertNotIsString' , 'assertNotString' }, + { 'assertNotIsTable' , 'assertNotTable' }, + { 'assertNotIsBoolean' , 'assertNotBoolean' }, + { 'assertNotIsNil' , 'assertNotNil' }, + { 'assertNotIsTrue' , 'assertNotTrue' }, + { 'assertNotIsFalse' , 'assertNotFalse' }, + { 'assertNotIsNaN' , 'assertNotNaN' }, + { 'assertNotIsInf' , 'assertNotInf' }, + { 'assertNotIsPlusInf' , 'assertNotPlusInf' }, + { 'assertNotIsMinusInf' , 'assertNotMinusInf' }, + { 'assertNotIsPlusZero' , 'assertNotPlusZero' }, + { 'assertNotIsMinusZero' , 'assertNotMinusZero' }, + { 'assertNotIsFunction' , 'assertNotFunction' }, + { 'assertNotIsThread' , 'assertNotThread' }, + { 'assertNotIsUserdata' , 'assertNotUserdata' }, + + -- type assertions: assertNotIsXXX -> assert_not_xxx + { 'assertNotIsNumber' , 'assert_not_number' }, + { 'assertNotIsString' , 'assert_not_string' }, + { 'assertNotIsTable' , 'assert_not_table' }, + { 'assertNotIsBoolean' , 'assert_not_boolean' }, + { 'assertNotIsNil' , 'assert_not_nil' }, + { 'assertNotIsTrue' , 'assert_not_true' }, + { 'assertNotIsFalse' , 'assert_not_false' }, + { 'assertNotIsNaN' , 'assert_not_nan' }, + { 'assertNotIsInf' , 'assert_not_inf' }, + { 'assertNotIsPlusInf' , 'assert_not_plus_inf' }, + { 'assertNotIsMinusInf' , 'assert_not_minus_inf' }, + { 'assertNotIsPlusZero' , 'assert_not_plus_zero' }, + { 'assertNotIsMinusZero' , 'assert_not_minus_zero' }, + { 'assertNotIsFunction' , 'assert_not_function' }, + { 'assertNotIsThread' , 'assert_not_thread' }, + { 'assertNotIsUserdata' , 'assert_not_userdata' }, + + -- all assertions with Coroutine duplicate Thread assertions + { 'assertIsThread' , 'assertIsCoroutine' }, + { 'assertIsThread' , 'assertCoroutine' }, + { 'assertIsThread' , 'assert_is_coroutine' }, + { 'assertIsThread' , 'assert_coroutine' }, + { 'assertNotIsThread' , 'assertNotIsCoroutine' }, + { 'assertNotIsThread' , 'assertNotCoroutine' }, + { 'assertNotIsThread' , 'assert_not_is_coroutine' }, + { 'assertNotIsThread' , 'assert_not_coroutine' }, +} + +-- Create all aliases in M +for _,v in ipairs( list_of_funcs ) do + local funcname, alias = v[1], v[2] + M[alias] = M[funcname] + + if EXPORT_ASSERT_TO_GLOBALS then + _G[funcname] = M[funcname] + _G[alias] = M[funcname] + end +end + +---------------------------------------------------------------- +-- +-- Outputters +-- +---------------------------------------------------------------- + +-- A common "base" class for outputters +-- For concepts involved (class inheritance) see http://www.lua.org/pil/16.2.html + +local genericOutput = { __class__ = 'genericOutput' } -- class +local genericOutput_MT = { __index = genericOutput } -- metatable +M.genericOutput = genericOutput -- publish, so that custom classes may derive from it + +function genericOutput.new(runner, default_verbosity) + -- runner is the "parent" object controlling the output, usually a LuaUnit instance + local t = { runner = runner } + if runner then + t.result = runner.result + t.verbosity = runner.verbosity or default_verbosity + t.fname = runner.fname + else + t.verbosity = default_verbosity + end + return setmetatable( t, genericOutput_MT) +end + +-- abstract ("empty") methods +function genericOutput:startSuite() + -- Called once, when the suite is started +end + +function genericOutput:startClass(className) + -- Called each time a new test class is started +end + +function genericOutput:startTest(testName) + -- called each time a new test is started, right before the setUp() + -- the current test status node is already created and available in: self.result.currentNode +end + +function genericOutput:updateStatus(node) + -- called with status failed or error as soon as the error/failure is encountered + -- this method is NOT called for a successful test because a test is marked as successful by default + -- and does not need to be updated +end + +function genericOutput:endTest(node) + -- called when the test is finished, after the tearDown() method +end + +function genericOutput:endClass() + -- called when executing the class is finished, before moving on to the next class of at the end of the test execution +end + +function genericOutput:endSuite() + -- called at the end of the test suite execution +end + + +---------------------------------------------------------------- +-- class TapOutput +---------------------------------------------------------------- + +local TapOutput = genericOutput.new() -- derived class +local TapOutput_MT = { __index = TapOutput } -- metatable +TapOutput.__class__ = 'TapOutput' + + -- For a good reference for TAP format, check: http://testanything.org/tap-specification.html + + function TapOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + return setmetatable( t, TapOutput_MT) + end + function TapOutput:startSuite() + print("1.."..self.result.selectedCount) + print('# Started on '..self.result.startDate) + end + function TapOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + + function TapOutput:updateStatus( node ) + if node:isSkipped() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t# SKIP ", node.msg, "\n" ) + return + end + + io.stdout:write("not ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + if self.verbosity > M.VERBOSITY_LOW then + print( prefixString( '# ', node.msg ) ) + end + if (node:isFailure() or node:isError()) and self.verbosity > M.VERBOSITY_DEFAULT then + print( prefixString( '# ', node.stackTrace ) ) + end + end + + function TapOutput:endTest( node ) + if node:isSuccess() then + io.stdout:write("ok ", self.result.currentTestNumber, "\t", node.testName, "\n") + end + end + + function TapOutput:endSuite() + print( '# '..M.LuaUnit.statusLine( self.result ) ) + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class JUnitOutput +---------------------------------------------------------------- + +-- See directory junitxml for more information about the junit format +local JUnitOutput = genericOutput.new() -- derived class +local JUnitOutput_MT = { __index = JUnitOutput } -- metatable +JUnitOutput.__class__ = 'JUnitOutput' + + function JUnitOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_LOW) + t.testList = {} + return setmetatable( t, JUnitOutput_MT ) + end + + function JUnitOutput:startSuite() + -- open xml file early to deal with errors + if self.fname == nil then + error('With Junit, an output filename must be supplied with --name!') + end + if string.sub(self.fname,-4) ~= '.xml' then + self.fname = self.fname..'.xml' + end + self.fd = io.open(self.fname, "w") + if self.fd == nil then + error("Could not open file for writing: "..self.fname) + end + + print('# XML output to '..self.fname) + print('# Started on '..self.result.startDate) + end + function JUnitOutput:startClass(className) + if className ~= '[TestFunctions]' then + print('# Starting class: '..className) + end + end + function JUnitOutput:startTest(testName) + print('# Starting test: '..testName) + end + + function JUnitOutput:updateStatus( node ) + if node:isFailure() then + print( '# Failure: ' .. prefixString( '# ', node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + elseif node:isError() then + print( '# Error: ' .. prefixString( '# ' , node.msg ):sub(4, nil) ) + -- print('# ' .. node.stackTrace) + end + end + + function JUnitOutput:endSuite() + print( '# '..M.LuaUnit.statusLine(self.result)) + + -- XML file writing + self.fd:write('\n') + self.fd:write('\n') + self.fd:write(string.format( + ' \n', + self.result.runCount, self.result.startIsodate, self.result.duration, self.result.errorCount, self.result.failureCount, self.result.skippedCount )) + self.fd:write(" \n") + self.fd:write(string.format(' \n', _VERSION ) ) + self.fd:write(string.format(' \n', M.VERSION) ) + -- XXX please include system name and version if possible + self.fd:write(" \n") + + for i,node in ipairs(self.result.allTests) do + self.fd:write(string.format(' \n', + node.className, node.testName, node.duration ) ) + if node:isNotSuccess() then + self.fd:write(node:statusXML()) + end + self.fd:write(' \n') + end + + -- Next two lines are needed to validate junit ANT xsd, but really not useful in general: + self.fd:write(' \n') + self.fd:write(' \n') + + self.fd:write(' \n') + self.fd:write('\n') + self.fd:close() + return self.result.notSuccessCount + end + + +-- class TapOutput end + +---------------------------------------------------------------- +-- class TextOutput +---------------------------------------------------------------- + +--[[ Example of other unit-tests suite text output + +-- Python Non verbose: + +For each test: . or F or E + +If some failed tests: + ============== + ERROR / FAILURE: TestName (testfile.testclass) + --------- + Stack trace + + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Python Verbose: +testname (filename.classname) ... ok +testname (filename.classname) ... FAIL +testname (filename.classname) ... ERROR + +then -------------- +then "Ran x tests in 0.000s" +then OK or FAILED (failures=1, error=1) + +-- Ruby: +Started + . + Finished in 0.002695 seconds. + + 1 tests, 2 assertions, 0 failures, 0 errors + +-- Ruby: +>> ruby tc_simple_number2.rb +Loaded suite tc_simple_number2 +Started +F.. +Finished in 0.038617 seconds. + + 1) Failure: +test_failure(TestSimpleNumber) [tc_simple_number2.rb:16]: +Adding doesn't work. +<3> expected but was +<4>. + +3 tests, 4 assertions, 1 failures, 0 errors + +-- Java Junit +.......F. +Time: 0,003 +There was 1 failure: +1) testCapacity(junit.samples.VectorTest)junit.framework.AssertionFailedError + at junit.samples.VectorTest.testCapacity(VectorTest.java:87) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + +FAILURES!!! +Tests run: 8, Failures: 1, Errors: 0 + + +-- Maven + +# mvn test +------------------------------------------------------- + T E S T S +------------------------------------------------------- +Running math.AdditionTest +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: +0.03 sec <<< FAILURE! + +Results : + +Failed tests: + testLireSymbole(math.AdditionTest) + +Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 + + +-- LuaUnit +---- non verbose +* display . or F or E when running tests +---- verbose +* display test name + ok/fail +---- +* blank line +* number) ERROR or FAILURE: TestName + Stack trace +* blank line +* number) ERROR or FAILURE: TestName + Stack trace + +then -------------- +then "Ran x tests in 0.000s (%d not selected, %d skipped)" +then OK or FAILED (failures=1, error=1) + + +]] + +local TextOutput = genericOutput.new() -- derived class +local TextOutput_MT = { __index = TextOutput } -- metatable +TextOutput.__class__ = 'TextOutput' + + function TextOutput.new(runner) + local t = genericOutput.new(runner, M.VERBOSITY_DEFAULT) + t.errorList = {} + return setmetatable( t, TextOutput_MT ) + end + + function TextOutput:startSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print( 'Started on '.. self.result.startDate ) + end + end + + function TextOutput:startTest(testName) + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write( " ", self.result.currentNode.testName, " ... " ) + end + end + + function TextOutput:endTest( node ) + if node:isSuccess() then + if self.verbosity > M.VERBOSITY_DEFAULT then + io.stdout:write("Ok\n") + else + io.stdout:write(".") + io.stdout:flush() + end + else + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.status ) + print( node.msg ) + --[[ + -- find out when to do this: + if self.verbosity > M.VERBOSITY_DEFAULT then + print( node.stackTrace ) + end + ]] + else + -- write only the first character of status E, F or S + io.stdout:write(string.sub(node.status, 1, 1)) + io.stdout:flush() + end + end + end + + function TextOutput:displayOneFailedTest( index, fail ) + print(index..") "..fail.testName ) + print( fail.msg ) + print( fail.stackTrace ) + print() + end + + function TextOutput:displayErroredTests() + if #self.result.errorTests ~= 0 then + print("Tests with errors:") + print("------------------") + for i, v in ipairs(self.result.errorTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:displayFailedTests() + if #self.result.failedTests ~= 0 then + print("Failed tests:") + print("-------------") + for i, v in ipairs(self.result.failedTests) do + self:displayOneFailedTest(i, v) + end + end + end + + function TextOutput:endSuite() + if self.verbosity > M.VERBOSITY_DEFAULT then + print("=========================================================") + else + print() + end + self:displayErroredTests() + self:displayFailedTests() + print( M.LuaUnit.statusLine( self.result ) ) + if self.result.notSuccessCount == 0 then + print('OK') + end + end + +-- class TextOutput end + + +---------------------------------------------------------------- +-- class NilOutput +---------------------------------------------------------------- + +local function nopCallable() + --print(42) + return nopCallable +end + +local NilOutput = { __class__ = 'NilOuptut' } -- class +local NilOutput_MT = { __index = nopCallable } -- metatable + +function NilOutput.new(runner) + return setmetatable( { __class__ = 'NilOutput' }, NilOutput_MT ) +end + +---------------------------------------------------------------- +-- +-- class LuaUnit +-- +---------------------------------------------------------------- + +M.LuaUnit = { + outputType = TextOutput, + verbosity = M.VERBOSITY_DEFAULT, + __class__ = 'LuaUnit' +} +local LuaUnit_MT = { __index = M.LuaUnit } + +if EXPORT_ASSERT_TO_GLOBALS then + LuaUnit = M.LuaUnit +end + + function M.LuaUnit.new() + return setmetatable( {}, LuaUnit_MT ) + end + + -----------------[[ Utility methods ]]--------------------- + + function M.LuaUnit.asFunction(aObject) + -- return "aObject" if it is a function, and nil otherwise + if 'function' == type(aObject) then + return aObject + end + end + + function M.LuaUnit.splitClassMethod(someName) + --[[ + Return a pair of className, methodName strings for a name in the form + "class.method". If no class part (or separator) is found, will return + nil, someName instead (the latter being unchanged). + + This convention thus also replaces the older isClassMethod() test: + You just have to check for a non-nil className (return) value. + ]] + local separator = string.find(someName, '.', 1, true) + if separator then + return someName:sub(1, separator - 1), someName:sub(separator + 1) + end + return nil, someName + end + + function M.LuaUnit.isMethodTestName( s ) + -- return true is the name matches the name of a test method + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.isTestName( s ) + -- return true is the name matches the name of a test + -- default rule is that is starts with 'Test' or with 'test' + return string.sub(s, 1, 4):lower() == 'test' + end + + function M.LuaUnit.collectTests() + -- return a list of all test names in the global namespace + -- that match LuaUnit.isTestName + + local testNames = {} + for k, _ in pairs(_G) do + if type(k) == "string" and M.LuaUnit.isTestName( k ) then + table.insert( testNames , k ) + end + end + table.sort( testNames ) + return testNames + end + + function M.LuaUnit.parseCmdLine( cmdLine ) + -- parse the command line + -- Supported command line parameters: + -- --verbose, -v: increase verbosity + -- --quiet, -q: silence output + -- --error, -e: treat errors as fatal (quit program) + -- --output, -o, + name: select output type + -- --pattern, -p, + pattern: run test matching pattern, may be repeated + -- --exclude, -x, + pattern: run test not matching pattern, may be repeated + -- --shuffle, -s, : shuffle tests before reunning them + -- --name, -n, + fname: name of output file for junit, default to stdout + -- --repeat, -r, + num: number of times to execute each test + -- [testnames, ...]: run selected test names + -- + -- Returns a table with the following fields: + -- verbosity: nil, M.VERBOSITY_DEFAULT, M.VERBOSITY_QUIET, M.VERBOSITY_VERBOSE + -- output: nil, 'tap', 'junit', 'text', 'nil' + -- testNames: nil or a list of test names to run + -- exeRepeat: num or 1 + -- pattern: nil or a list of patterns + -- exclude: nil or a list of patterns + + local result, state = {}, nil + local SET_OUTPUT = 1 + local SET_PATTERN = 2 + local SET_EXCLUDE = 3 + local SET_FNAME = 4 + local SET_REPEAT = 5 + + if cmdLine == nil then + return result + end + + local function parseOption( option ) + if option == '--help' or option == '-h' then + result['help'] = true + return + elseif option == '--version' then + result['version'] = true + return + elseif option == '--verbose' or option == '-v' then + result['verbosity'] = M.VERBOSITY_VERBOSE + return + elseif option == '--quiet' or option == '-q' then + result['verbosity'] = M.VERBOSITY_QUIET + return + elseif option == '--error' or option == '-e' then + result['quitOnError'] = true + return + elseif option == '--failure' or option == '-f' then + result['quitOnFailure'] = true + return + elseif option == '--shuffle' or option == '-s' then + result['shuffle'] = true + return + elseif option == '--output' or option == '-o' then + state = SET_OUTPUT + return state + elseif option == '--name' or option == '-n' then + state = SET_FNAME + return state + elseif option == '--repeat' or option == '-r' then + state = SET_REPEAT + return state + elseif option == '--pattern' or option == '-p' then + state = SET_PATTERN + return state + elseif option == '--exclude' or option == '-x' then + state = SET_EXCLUDE + return state + end + error('Unknown option: '..option,3) + end + + local function setArg( cmdArg, state ) + if state == SET_OUTPUT then + result['output'] = cmdArg + return + elseif state == SET_FNAME then + result['fname'] = cmdArg + return + elseif state == SET_REPEAT then + result['exeRepeat'] = tonumber(cmdArg) + or error('Malformed -r argument: '..cmdArg) + return + elseif state == SET_PATTERN then + if result['pattern'] then + table.insert( result['pattern'], cmdArg ) + else + result['pattern'] = { cmdArg } + end + return + elseif state == SET_EXCLUDE then + local notArg = '!'..cmdArg + if result['pattern'] then + table.insert( result['pattern'], notArg ) + else + result['pattern'] = { notArg } + end + return + end + error('Unknown parse state: '.. state) + end + + + for i, cmdArg in ipairs(cmdLine) do + if state ~= nil then + setArg( cmdArg, state, result ) + state = nil + else + if cmdArg:sub(1,1) == '-' then + state = parseOption( cmdArg ) + else + if result['testNames'] then + table.insert( result['testNames'], cmdArg ) + else + result['testNames'] = { cmdArg } + end + end + end + end + + if result['help'] then + M.LuaUnit.help() + end + + if result['version'] then + M.LuaUnit.version() + end + + if state ~= nil then + error('Missing argument after '..cmdLine[ #cmdLine ],2 ) + end + + return result + end + + function M.LuaUnit.help() + print(M.USAGE) + os.exit(0) + end + + function M.LuaUnit.version() + print('LuaUnit v'..M.VERSION..' by Philippe Fremy ') + os.exit(0) + end + +---------------------------------------------------------------- +-- class NodeStatus +---------------------------------------------------------------- + + local NodeStatus = { __class__ = 'NodeStatus' } -- class + local NodeStatus_MT = { __index = NodeStatus } -- metatable + M.NodeStatus = NodeStatus + + -- values of status + NodeStatus.SUCCESS = 'SUCCESS' + NodeStatus.SKIP = 'SKIP' + NodeStatus.FAIL = 'FAIL' + NodeStatus.ERROR = 'ERROR' + + function NodeStatus.new( number, testName, className ) + -- default constructor, test are PASS by default + local t = { number = number, testName = testName, className = className } + setmetatable( t, NodeStatus_MT ) + t:success() + return t + end + + function NodeStatus:success() + self.status = self.SUCCESS + -- useless because lua does this for us, but it helps me remembering the relevant field names + self.msg = nil + self.stackTrace = nil + end + + function NodeStatus:skip(msg) + self.status = self.SKIP + self.msg = msg + self.stackTrace = nil + end + + function NodeStatus:fail(msg, stackTrace) + self.status = self.FAIL + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:error(msg, stackTrace) + self.status = self.ERROR + self.msg = msg + self.stackTrace = stackTrace + end + + function NodeStatus:isSuccess() + return self.status == NodeStatus.SUCCESS + end + + function NodeStatus:isNotSuccess() + -- Return true if node is either failure or error or skip + return (self.status == NodeStatus.FAIL or self.status == NodeStatus.ERROR or self.status == NodeStatus.SKIP) + end + + function NodeStatus:isSkipped() + return self.status == NodeStatus.SKIP + end + + function NodeStatus:isFailure() + return self.status == NodeStatus.FAIL + end + + function NodeStatus:isError() + return self.status == NodeStatus.ERROR + end + + function NodeStatus:statusXML() + if self:isError() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isFailure() then + return table.concat( + {' \n', + ' \n'}) + elseif self:isSkipped() then + return table.concat({' ', xmlEscape(self.msg),'\n' } ) + end + return ' \n' -- (not XSD-compliant! normally shouldn't get here) + end + + --------------[[ Output methods ]]------------------------- + + local function conditional_plural(number, singular) + -- returns a grammatically well-formed string "%d " + local suffix = '' + if number ~= 1 then -- use plural + suffix = (singular:sub(-2) == 'ss') and 'es' or 's' + end + return string.format('%d %s%s', number, singular, suffix) + end + + function M.LuaUnit.statusLine(result) + -- return status line string according to results + local s = { + string.format('Ran %d tests in %0.3f seconds', + result.runCount, result.duration), + conditional_plural(result.successCount, 'success'), + } + if result.notSuccessCount > 0 then + if result.failureCount > 0 then + table.insert(s, conditional_plural(result.failureCount, 'failure')) + end + if result.errorCount > 0 then + table.insert(s, conditional_plural(result.errorCount, 'error')) + end + else + table.insert(s, '0 failures') + end + if result.skippedCount > 0 then + table.insert(s, string.format("%d skipped", result.skippedCount)) + end + if result.nonSelectedCount > 0 then + table.insert(s, string.format("%d non-selected", result.nonSelectedCount)) + end + return table.concat(s, ', ') + end + + function M.LuaUnit:startSuite(selectedCount, nonSelectedCount) + self.result = { + selectedCount = selectedCount, + nonSelectedCount = nonSelectedCount, + successCount = 0, + runCount = 0, + currentTestNumber = 0, + currentClassName = "", + currentNode = nil, + suiteStarted = true, + startTime = os.clock(), + startDate = os.date(os.getenv('LUAUNIT_DATEFMT')), + startIsodate = os.date('%Y-%m-%dT%H:%M:%S'), + patternIncludeFilter = self.patternIncludeFilter, + + -- list of test node status + allTests = {}, + failedTests = {}, + errorTests = {}, + skippedTests = {}, + + failureCount = 0, + errorCount = 0, + notSuccessCount = 0, + skippedCount = 0, + } + + self.outputType = self.outputType or TextOutput + self.output = self.outputType.new(self) + self.output:startSuite() + end + + function M.LuaUnit:startClass( className ) + self.result.currentClassName = className + self.output:startClass( className ) + end + + function M.LuaUnit:startTest( testName ) + self.result.currentTestNumber = self.result.currentTestNumber + 1 + self.result.runCount = self.result.runCount + 1 + self.result.currentNode = NodeStatus.new( + self.result.currentTestNumber, + testName, + self.result.currentClassName + ) + self.result.currentNode.startTime = os.clock() + table.insert( self.result.allTests, self.result.currentNode ) + self.output:startTest( testName ) + end + + function M.LuaUnit:updateStatus( err ) + -- "err" is expected to be a table / result from protectedCall() + if err.status == NodeStatus.SUCCESS then + return + end + + local node = self.result.currentNode + + --[[ As a first approach, we will report only one error or one failure for one test. + + However, we can have the case where the test is in failure, and the teardown is in error. + In such case, it's a good idea to report both a failure and an error in the test suite. This is + what Python unittest does for example. However, it mixes up counts so need to be handled carefully: for + example, there could be more (failures + errors) count that tests. What happens to the current node ? + + We will do this more intelligent version later. + ]] + + -- if the node is already in failure/error, just don't report the new error (see above) + if node.status ~= NodeStatus.SUCCESS then + return + end + + if err.status == NodeStatus.FAIL then + node:fail( err.msg, err.trace ) + table.insert( self.result.failedTests, node ) + elseif err.status == NodeStatus.ERROR then + node:error( err.msg, err.trace ) + table.insert( self.result.errorTests, node ) + elseif err.status == NodeStatus.SKIP then + node:skip( err.msg ) + table.insert( self.result.skippedTests, node ) + else + error('No such status: ' .. prettystr(err.status)) + end + + self.output:updateStatus( node ) + end + + function M.LuaUnit:endTest() + local node = self.result.currentNode + -- print( 'endTest() '..prettystr(node)) + -- print( 'endTest() '..prettystr(node:isNotSuccess())) + node.duration = os.clock() - node.startTime + node.startTime = nil + self.output:endTest( node ) + + if node:isSuccess() then + self.result.successCount = self.result.successCount + 1 + elseif node:isError() then + if self.quitOnError or self.quitOnFailure then + -- Runtime error - abort test execution as requested by + -- "--error" option. This is done by setting a special + -- flag that gets handled in runSuiteByInstances(). + print("\nERROR during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isFailure() then + if self.quitOnFailure then + -- Failure - abort test execution as requested by + -- "--failure" option. This is done by setting a special + -- flag that gets handled in runSuiteByInstances(). + print("\nFailure during LuaUnit test execution:\n" .. node.msg) + self.result.aborted = true + end + elseif node:isSkipped() then + self.result.runCount = self.result.runCount - 1 + else + error('No such node status: ' .. prettystr(node.status)) + end + self.result.currentNode = nil + end + + function M.LuaUnit:endClass() + self.output:endClass() + end + + function M.LuaUnit:endSuite() + if self.result.suiteStarted == false then + error('LuaUnit:endSuite() -- suite was already ended' ) + end + self.result.duration = os.clock()-self.result.startTime + self.result.suiteStarted = false + + -- Expose test counts for outputter's endSuite(). This could be managed + -- internally instead by using the length of the lists of failed tests + -- but unit tests rely on these fields being present. + self.result.failureCount = #self.result.failedTests + self.result.errorCount = #self.result.errorTests + self.result.notSuccessCount = self.result.failureCount + self.result.errorCount + self.result.skippedCount = #self.result.skippedTests + + self.output:endSuite() + end + + function M.LuaUnit:setOutputType(outputType, fname) + -- Configures LuaUnit runner output + -- outputType is one of: NIL, TAP, JUNIT, TEXT + -- when outputType is junit, the additional argument fname is used to set the name of junit output file + -- for other formats, fname is ignored + if outputType:upper() == "NIL" then + self.outputType = NilOutput + return + end + if outputType:upper() == "TAP" then + self.outputType = TapOutput + return + end + if outputType:upper() == "JUNIT" then + self.outputType = JUnitOutput + if fname then + self.fname = fname + end + return + end + if outputType:upper() == "TEXT" then + self.outputType = TextOutput + return + end + error( 'No such format: '..outputType,2) + end + + --------------[[ Runner ]]----------------- + + function M.LuaUnit:protectedCall(classInstance, methodInstance, prettyFuncName) + -- if classInstance is nil, this is just a function call + -- else, it's method of a class being called. + + local function err_handler(e) + -- transform error into a table, adding the traceback information + return { + status = NodeStatus.ERROR, + msg = e, + trace = string.sub(debug.traceback("", 1), 2) + } + end + + local ok, err + if classInstance then + -- stupid Lua < 5.2 does not allow xpcall with arguments so let's use a workaround + ok, err = xpcall( function () methodInstance(classInstance) end, err_handler ) + else + ok, err = xpcall( function () methodInstance() end, err_handler ) + end + if ok then + return {status = NodeStatus.SUCCESS} + end + -- print('ok="'..prettystr(ok)..'" err="'..prettystr(err)..'"') + + local iter_msg + iter_msg = self.exeRepeat and 'iteration '..self.currentCount + + err.msg, err.status = M.adjust_err_msg_with_iter( err.msg, iter_msg ) + + if err.status == NodeStatus.SUCCESS or err.status == NodeStatus.SKIP then + err.trace = nil + return err + end + + -- reformat / improve the stack trace + if prettyFuncName then -- we do have the real method name + err.trace = err.trace:gsub("in (%a+) 'methodInstance'", "in %1 '"..prettyFuncName.."'") + end + if STRIP_LUAUNIT_FROM_STACKTRACE then + err.trace = stripLuaunitTrace2(err.trace, err.msg) + end + + return err -- return the error "object" (table) + end + + + function M.LuaUnit:execOneFunction(className, methodName, classInstance, methodInstance) + -- When executing a test function, className and classInstance must be nil + -- When executing a class method, all parameters must be set + + if type(methodInstance) ~= 'function' then + error( tostring(methodName)..' must be a function, not '..type(methodInstance)) + end + + local prettyFuncName + if className == nil then + className = '[TestFunctions]' + prettyFuncName = methodName + else + prettyFuncName = className..'.'..methodName + end + + if self.lastClassName ~= className then + if self.lastClassName ~= nil then + self:endClass() + end + self:startClass( className ) + self.lastClassName = className + end + + self:startTest(prettyFuncName) + + local node = self.result.currentNode + for iter_n = 1, self.exeRepeat or 1 do + if node:isNotSuccess() then + break + end + self.currentCount = iter_n + + -- run setUp first (if any) + if classInstance then + local func = self.asFunction( classInstance.setUp ) or + self.asFunction( classInstance.Setup ) or + self.asFunction( classInstance.setup ) or + self.asFunction( classInstance.SetUp ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.setUp')) + end + end + + -- run testMethod() + if node:isSuccess() then + self:updateStatus(self:protectedCall(classInstance, methodInstance, prettyFuncName)) + end + + -- lastly, run tearDown (if any) + if classInstance then + local func = self.asFunction( classInstance.tearDown ) or + self.asFunction( classInstance.TearDown ) or + self.asFunction( classInstance.teardown ) or + self.asFunction( classInstance.Teardown ) + if func then + self:updateStatus(self:protectedCall(classInstance, func, className..'.tearDown')) + end + end + end + + self:endTest() + end + + function M.LuaUnit.expandOneClass( result, className, classInstance ) + --[[ + Input: a list of { name, instance }, a class name, a class instance + Ouptut: modify result to add all test method instance in the form: + { className.methodName, classInstance } + ]] + for methodName, methodInstance in sortedPairs(classInstance) do + if M.LuaUnit.asFunction(methodInstance) and M.LuaUnit.isMethodTestName( methodName ) then + table.insert( result, { className..'.'..methodName, classInstance } ) + end + end + end + + function M.LuaUnit.expandClasses( listOfNameAndInst ) + --[[ + -- expand all classes (provided as {className, classInstance}) to a list of {className.methodName, classInstance} + -- functions and methods remain untouched + + Input: a list of { name, instance } + + Output: + * { function name, function instance } : do nothing + * { class.method name, class instance }: do nothing + * { class name, class instance } : add all method names in the form of (className.methodName, classInstance) + ]] + local result = {} + + for i,v in ipairs( listOfNameAndInst ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + table.insert( result, { name, instance } ) + else + if type(instance) ~= 'table' then + error( 'Instance must be a table or a function, not a '..type(instance)..' with value '..prettystr(instance)) + end + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + local methodInstance = instance[methodName] + if methodInstance == nil then + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + table.insert( result, { name, instance } ) + else + M.LuaUnit.expandOneClass( result, name, instance ) + end + end + end + + return result + end + + function M.LuaUnit.applyPatternFilter( patternIncFilter, listOfNameAndInst ) + local included, excluded = {}, {} + for i, v in ipairs( listOfNameAndInst ) do + -- local name, instance = v[1], v[2] + if patternFilter( patternIncFilter, v[1] ) then + table.insert( included, v ) + else + table.insert( excluded, v ) + end + end + return included, excluded + end + + function M.LuaUnit:runSuiteByInstances( listOfNameAndInst ) + --[[ Run an explicit list of tests. Each item of the list must be one of: + * { function name, function instance } + * { class name, class instance } + * { class.method name, class instance } + ]] + + local expandedList = self.expandClasses( listOfNameAndInst ) + if self.shuffle then + randomizeTable( expandedList ) + end + local filteredList, filteredOutList = self.applyPatternFilter( + self.patternIncludeFilter, expandedList ) + + self:startSuite( #filteredList, #filteredOutList ) + + for i,v in ipairs( filteredList ) do + local name, instance = v[1], v[2] + if M.LuaUnit.asFunction(instance) then + self:execOneFunction( nil, name, nil, instance ) + else + -- expandClasses() should have already taken care of sanitizing the input + assert( type(instance) == 'table' ) + local className, methodName = M.LuaUnit.splitClassMethod( name ) + assert( className ~= nil ) + local methodInstance = instance[methodName] + assert(methodInstance ~= nil) + self:execOneFunction( className, methodName, instance, methodInstance ) + end + if self.result.aborted then + break -- "--error" or "--failure" option triggered + end + end + + if self.lastClassName ~= nil then + self:endClass() + end + + self:endSuite() + + if self.result.aborted then + print("LuaUnit ABORTED (as requested by --error or --failure option)") + os.exit(-2) + end + end + + function M.LuaUnit:runSuiteByNames( listOfName ) + --[[ Run LuaUnit with a list of generic names, coming either from command-line or from global + namespace analysis. Convert the list into a list of (name, valid instances (table or function)) + and calls runSuiteByInstances. + ]] + + local instanceName, instance + local listOfNameAndInst = {} + + for i,name in ipairs( listOfName ) do + local className, methodName = M.LuaUnit.splitClassMethod( name ) + if className then + instanceName = className + instance = _G[instanceName] + + if instance == nil then + error( "No such name in global space: "..instanceName ) + end + + if type(instance) ~= 'table' then + error( 'Instance of '..instanceName..' must be a table, not '..type(instance)) + end + + local methodInstance = instance[methodName] + if methodInstance == nil then + error( "Could not find method in class "..tostring(className).." for method "..tostring(methodName) ) + end + + else + -- for functions and classes + instanceName = name + instance = _G[instanceName] + end + + if instance == nil then + error( "No such name in global space: "..instanceName ) + end + + if (type(instance) ~= 'table' and type(instance) ~= 'function') then + error( 'Name must match a function or a table: '..instanceName ) + end + + table.insert( listOfNameAndInst, { name, instance } ) + end + + self:runSuiteByInstances( listOfNameAndInst ) + end + + function M.LuaUnit.run(...) + -- Run some specific test classes. + -- If no arguments are passed, run the class names specified on the + -- command line. If no class name is specified on the command line + -- run all classes whose name starts with 'Test' + -- + -- If arguments are passed, they must be strings of the class names + -- that you want to run or generic command line arguments (-o, -p, -v, ...) + + local runner = M.LuaUnit.new() + return runner:runSuite(...) + end + + function M.LuaUnit:runSuite( ... ) + + local args = {...} + if type(args[1]) == 'table' and args[1].__class__ == 'LuaUnit' then + -- run was called with the syntax M.LuaUnit:runSuite() + -- we support both M.LuaUnit.run() and M.LuaUnit:run() + -- strip out the first argument + table.remove(args,1) + end + + if #args == 0 then + args = cmdline_argv + end + + local options = pcall_or_abort( M.LuaUnit.parseCmdLine, args ) + + -- We expect these option fields to be either `nil` or contain + -- valid values, so it's safe to always copy them directly. + self.verbosity = options.verbosity + self.quitOnError = options.quitOnError + self.quitOnFailure = options.quitOnFailure + + self.exeRepeat = options.exeRepeat + self.patternIncludeFilter = options.pattern + self.shuffle = options.shuffle + + if options.output then + if options.output:lower() == 'junit' and options.fname == nil then + print('With junit output, a filename must be supplied with -n or --name') + os.exit(-1) + end + pcall_or_abort(self.setOutputType, self, options.output, options.fname) + end + + self:runSuiteByNames( options.testNames or M.LuaUnit.collectTests() ) + + return self.result.notSuccessCount + end +-- class LuaUnit + +-- For compatbility with LuaUnit v2 +M.run = M.LuaUnit.run +M.Run = M.LuaUnit.run + +function M:setVerbosity( verbosity ) + M.LuaUnit.verbosity = verbosity +end +M.set_verbosity = M.setVerbosity +M.SetVerbosity = M.setVerbosity + + +return M diff --git a/scripts/fetch-linux.sh b/scripts/fetch-linux.sh new file mode 100755 index 0000000..9a1c382 --- /dev/null +++ b/scripts/fetch-linux.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# script from https://blog.markvincze.com/download-artifacts-from-a-latest-github-release-in-sh-and-powershell/ +set -e +LATEST_RELEASE=$(curl -L -s -H 'Accept: application/json' https://github.com/launchdarkly/c-server-sdk/releases/latest) +LATEST_VERSION=$(echo $LATEST_RELEASE | sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/') +ARTIFACT_URL="https://github.com/launchdarkly/c-server-sdk/releases/download/$LATEST_VERSION/linux-gcc-64bit-dynamic.zip" +curl -o linux-gcc-64bit-dynamic.zip -L $ARTIFACT_URL +unzip linux-gcc-64bit-dynamic.zip diff --git a/test.lua b/test.lua new file mode 100644 index 0000000..5d3bad2 --- /dev/null +++ b/test.lua @@ -0,0 +1,39 @@ +local u = require('luaunit') +local l = require("launchdarkly-server-sdk") + +function makeTestClient() + return l.clientInit({ + key = "sdk-test", + offline = true + }, 0) +end + +local user = l.makeUser({ key = "alice" }) + +function testBoolVariation() + local e = false + u.assertEquals(e, makeTestClient().boolVariation(user, "test", e)) +end + +function testIntVariation() + local e = 3 + u.assertEquals(e, makeTestClient().intVariation(user, "test", e)) +end + +function testDoubleVariation() + local e = 5.3 + u.assertEquals(e, makeTestClient().doubleVariation(user, "test", e)) +end + +function testStringVariation() + local e = "a" + u.assertEquals(e, makeTestClient().stringVariation(user, "test", e)) +end + +function testJSONVariation() + local e = { ["a"] = "b" } + u.assertEquals(e, makeTestClient().jsonVariation(user, "test", e)) +end + +local runner = u.LuaUnit.new() +os.exit(runner:runSuite()) From 89806d4a736f2440a5c34e4198cffd1d8fed1260 Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 24 Jun 2020 15:23:03 -0700 Subject: [PATCH 16/25] [ch80958] remove beta warning --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index a5fe5b4..46a1e15 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ LaunchDarkly Server-Side SDK for Lua =========================== -*This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* - LaunchDarkly overview ------------------------- [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! From 6b6a4c5a99463e37fe8b956a9eeb748278c4292e Mon Sep 17 00:00:00 2001 From: hroederld Date: Thu, 25 Jun 2020 16:07:46 -0700 Subject: [PATCH 17/25] fix timeout (#17) --- launchdarkly-server-sdk.lua | 7 +++++-- test.lua | 13 ++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua index 06c4790..6a7a22e 100644 --- a/launchdarkly-server-sdk.lua +++ b/launchdarkly-server-sdk.lua @@ -446,10 +446,13 @@ end local function clientInit(config, timeoutMilliseconds) local interface = {} + if timeoutMilliseconds <= 0 then + timeoutMilliseconds = 1 + end + --- An opaque client object -- @type Client - - local client = ffi.gc(so.LDClientInit(makeConfig(config), 1000), so.LDClientClose) + local client = ffi.gc(so.LDClientInit(makeConfig(config), timeoutMilliseconds), so.LDClientClose) --- Check if a client has been fully initialized. This may be useful if the -- initialization timeout was reached. diff --git a/test.lua b/test.lua index 5d3bad2..5ec4e51 100644 --- a/test.lua +++ b/test.lua @@ -1,11 +1,18 @@ local u = require('luaunit') local l = require("launchdarkly-server-sdk") +function logger(level, line) + print(level .. ": " .. line) +end + +l.registerLogger("TRACE", logger) + function makeTestClient() - return l.clientInit({ - key = "sdk-test", - offline = true + local c = l.clientInit({ + key = "sdk-test" }, 0) + + return c end local user = l.makeUser({ key = "alice" }) From 39da875c5abe01d453749805d49eabce8e1a55de Mon Sep 17 00:00:00 2001 From: hroederld Date: Tue, 7 Jul 2020 14:01:11 -0700 Subject: [PATCH 18/25] [ch70870] redis module (#18) --- .circleci/config.yml | 16 ++++++-- .ldrelease/linux-prepare.sh | 3 +- launchdarkly-server-sdk-redis.lua | 63 +++++++++++++++++++++++++++++++ launchdarkly-server-sdk.lua | 8 +++- scripts/fetch-linux.sh | 8 ---- test.lua | 12 ++++++ 6 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 launchdarkly-server-sdk-redis.lua delete mode 100755 scripts/fetch-linux.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index da41be4..5bc7a42 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,6 +10,7 @@ jobs: build-test-linux: docker: - image: ubuntu:18.04 + - image: redis steps: - checkout - run: @@ -19,8 +20,17 @@ jobs: name: Build Doc command: ./.ldrelease/linux-build-docs.sh - run: - name: Fetch c-server-sdk - command: ./scripts/fetch-linux.sh + name: Build c-server-sdk + command: | + git clone https://github.com/launchdarkly/c-server-sdk.git + cd c-server-sdk + mkdir build + cd build + cmake -D REDIS_STORE=ON -D BUILD_SHARED_LIBS=ON -D BUILD_TESTING=OFF .. + make - run: name: Run tests - command: LD_LIBRARY_PATH=./lib luajit test.lua + command: | + cp c-server-sdk/build/libldserverapi.so . + cp c-server-sdk/build/stores/redis/libldserverapi-redis.so . + LD_LIBRARY_PATH=. luajit test.lua diff --git a/.ldrelease/linux-prepare.sh b/.ldrelease/linux-prepare.sh index b9e7621..1cdb702 100755 --- a/.ldrelease/linux-prepare.sh +++ b/.ldrelease/linux-prepare.sh @@ -3,4 +3,5 @@ set -e apt-get update -y && apt-get install -y luajit lua-ldoc zip ca-certificates \ - curl zip lua-cjson libpcre3 libcurl4-openssl-dev + curl zip lua-cjson libpcre3 libcurl4-openssl-dev cmake libhiredis-dev git \ + build-essential libpcre3-dev diff --git a/launchdarkly-server-sdk-redis.lua b/launchdarkly-server-sdk-redis.lua new file mode 100644 index 0000000..e1b13fb --- /dev/null +++ b/launchdarkly-server-sdk-redis.lua @@ -0,0 +1,63 @@ +--- Server-side SDK for LaunchDarkly Redis store. +-- @module launchdarkly-server-sdk-redis + +local ffi = require("ffi") + +ffi.cdef[[ + struct LDRedisConfig; + struct LDStoreInterface; + + struct LDRedisConfig *LDRedisConfigNew(); + + bool LDRedisConfigSetHost( + struct LDRedisConfig *const config, + const char *const host); + + bool LDRedisConfigSetPort( + struct LDRedisConfig *const config, + const unsigned short port); + + bool LDRedisConfigSetPrefix( + struct LDRedisConfig *const config, + const char *const prefix); + + bool LDRedisConfigSetPoolSize( + struct LDRedisConfig *const config, + const unsigned int poolSize); + + bool LDRedisConfigFree(struct LDRedisConfig *const config); + + struct LDStoreInterface *LDStoreInterfaceRedisNew( + struct LDRedisConfig *const config); +]] + +local so = ffi.load("ldserverapi-redis") + +local function applyWhenNotNil(subject, operation, value) + if value ~= nil and value ~= cjson.null then + operation(subject, value) + end +end + +--- Initialize a store backend +-- @tparam table fields list of configuration options +-- @tparam[opt] string fields.host Hostname for Redis. +-- @tparam[opt] int fields.port Port for Redis. +-- @tparam[opt] string fields.prefix Redis key prefix for SDK values. +-- @tparam[opt] int fields.poolSize Number of Redis connections to maintain. +-- @return A fresh Redis store backend. +local function makeStore(fields) + local config = so.LDRedisConfigNew() + + applyWhenNotNil(config, so.LDRedisConfigSetHost, fields["host"]) + applyWhenNotNil(config, so.LDRedisConfigSetPort, fields["port"]) + applyWhenNotNil(config, so.LDRedisConfigSetPrefix, fields["prefix"]) + applyWhenNotNil(config, so.LDRedisConfigSetPoolSize, fields["poolSize"]) + + return so.LDStoreInterfaceRedisNew(config) +end + +-- @export +return { + makeStore = makeStore +} diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua index 6a7a22e..e2bb656 100644 --- a/launchdarkly-server-sdk.lua +++ b/launchdarkly-server-sdk.lua @@ -335,6 +335,7 @@ local function makeConfig(fields) applyWhenNotNil(config, so.LDConfigInlineUsersInEvents, fields["inlineUsersInEvents"]) applyWhenNotNil(config, so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) applyWhenNotNil(config, so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) + applyWhenNotNil(config, so.LDConfigSetFeatureStoreBackend, fields["featureStoreBackend"]) so.LDConfigSetWrapperInfo(config, "lua-server-sdk", SDKVersion) @@ -440,8 +441,11 @@ end -- @tparam[opt] table config.privateAttributeNames Marks a set of user attribute -- names private. Any users sent to LaunchDarkly with this configuration active -- will have attributes with these names removed. --- @tparam int timeoutMilliseconds How long to wait for flags to download. --- If the timeout is reached a non fully initialized client will be returned. +-- @tparam[opt] backend config.featureStoreBackend Persistent feature store +-- backend. +-- @tparam int timeoutMilliseconds How long to wait for flags to +-- download. If the timeout is reached a non fully initialized client will be +-- returned. -- @return A fresh client. local function clientInit(config, timeoutMilliseconds) local interface = {} diff --git a/scripts/fetch-linux.sh b/scripts/fetch-linux.sh deleted file mode 100755 index 9a1c382..0000000 --- a/scripts/fetch-linux.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -# script from https://blog.markvincze.com/download-artifacts-from-a-latest-github-release-in-sh-and-powershell/ -set -e -LATEST_RELEASE=$(curl -L -s -H 'Accept: application/json' https://github.com/launchdarkly/c-server-sdk/releases/latest) -LATEST_VERSION=$(echo $LATEST_RELEASE | sed -e 's/.*"tag_name":"\([^"]*\)".*/\1/') -ARTIFACT_URL="https://github.com/launchdarkly/c-server-sdk/releases/download/$LATEST_VERSION/linux-gcc-64bit-dynamic.zip" -curl -o linux-gcc-64bit-dynamic.zip -L $ARTIFACT_URL -unzip linux-gcc-64bit-dynamic.zip diff --git a/test.lua b/test.lua index 5ec4e51..97a7257 100644 --- a/test.lua +++ b/test.lua @@ -1,5 +1,6 @@ local u = require('luaunit') local l = require("launchdarkly-server-sdk") +local r = require("launchdarkly-server-sdk-redis") function logger(level, line) print(level .. ": " .. line) @@ -42,5 +43,16 @@ function testJSONVariation() u.assertEquals(e, makeTestClient().jsonVariation(user, "test", e)) end +function testRedisBasic() + local c = l.clientInit({ + key = "sdk-test", + featureStoreBackend = r.makeStore({}) + }, 0) + + local e = false + + u.assertEquals(e, makeTestClient().boolVariation(user, "test", e)) +end + local runner = u.LuaUnit.new() os.exit(runner:runSuite()) From cbc133f7a7664209f9899b9bcc1c962da262195c Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 8 Jul 2020 17:28:08 -0700 Subject: [PATCH 19/25] [ch70870] ldoc releaser redis (#19) --- .ldrelease/linux-build-docs.sh | 6 +++++- launchdarkly-server-sdk-redis.lua | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.ldrelease/linux-build-docs.sh b/.ldrelease/linux-build-docs.sh index 91557bb..d753a86 100755 --- a/.ldrelease/linux-build-docs.sh +++ b/.ldrelease/linux-build-docs.sh @@ -6,7 +6,11 @@ set -e PROJECT_DIR=$(pwd) -ldoc launchdarkly-server-sdk.lua +mkdir tmp +cp launchdarkly-server-sdk.lua tmp/ +cp launchdarkly-server-sdk-redis.lua tmp/ + +ldoc tmp mkdir -p $PROJECT_DIR/artifacts cd $PROJECT_DIR/doc diff --git a/launchdarkly-server-sdk-redis.lua b/launchdarkly-server-sdk-redis.lua index e1b13fb..3551f5f 100644 --- a/launchdarkly-server-sdk-redis.lua +++ b/launchdarkly-server-sdk-redis.lua @@ -57,7 +57,7 @@ local function makeStore(fields) return so.LDStoreInterfaceRedisNew(config) end --- @export +--- @export return { makeStore = makeStore } From ad96c4e1475c0fb4ac52f902d9ef0240685c2665 Mon Sep 17 00:00:00 2001 From: hroederld Date: Wed, 8 Jul 2020 21:15:51 -0700 Subject: [PATCH 20/25] [ch82399] document c server-side sdk version requirement (#20) --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 46a1e15..633d131 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ Supported Lua versions This version of the LaunchDarkly SDK is compatible with the Lua 5.1 interpreter, and LuaJIT. Lua 5.3 is not supported due to FFI constraints. +Supported C server-side SDK versions +----------- + +This version of the Lua server-side SDK depends on the C server-side SDK. The minimum required version is `2.1.0`, and under `3.0.0`. + Getting started ----------- From 5eb0fdca1aa29169f3a7f68222d4a176c265167d Mon Sep 17 00:00:00 2001 From: hroederld Date: Fri, 10 Jul 2020 13:07:26 -0700 Subject: [PATCH 21/25] [ch82565] collect between tests (#21) --- test.lua | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/test.lua b/test.lua index 97a7257..d8c267d 100644 --- a/test.lua +++ b/test.lua @@ -8,9 +8,12 @@ end l.registerLogger("TRACE", logger) +TestAll = {} + function makeTestClient() local c = l.clientInit({ - key = "sdk-test" + key = "sdk-test", + offline = true }, 0) return c @@ -18,35 +21,40 @@ end local user = l.makeUser({ key = "alice" }) -function testBoolVariation() +function TestAll:tearDown() + collectgarbage("collect") +end + +function TestAll:testBoolVariation() local e = false u.assertEquals(e, makeTestClient().boolVariation(user, "test", e)) end -function testIntVariation() +function TestAll:testIntVariation() local e = 3 u.assertEquals(e, makeTestClient().intVariation(user, "test", e)) end -function testDoubleVariation() +function TestAll:testDoubleVariation() local e = 5.3 u.assertEquals(e, makeTestClient().doubleVariation(user, "test", e)) end -function testStringVariation() +function TestAll:testStringVariation() local e = "a" u.assertEquals(e, makeTestClient().stringVariation(user, "test", e)) end -function testJSONVariation() +function TestAll:testJSONVariation() local e = { ["a"] = "b" } u.assertEquals(e, makeTestClient().jsonVariation(user, "test", e)) end -function testRedisBasic() +function TestAll:testRedisBasic() local c = l.clientInit({ key = "sdk-test", - featureStoreBackend = r.makeStore({}) + featureStoreBackend = r.makeStore({}), + offline = true }, 0) local e = false From c5ab6f272eca64342b7e54543f386e6603a047ce Mon Sep 17 00:00:00 2001 From: hroederld Date: Mon, 20 Jul 2020 13:00:26 -0700 Subject: [PATCH 22/25] [ch82639] rewrite for lua 5.1 5.2 5.3 (#22) --- .circleci/config.yml | 89 +- .ldrelease/build-c-server.sh | 11 + .ldrelease/linux-build-docs.sh | 4 +- launchdarkly-server-sdk-1.0-0.rockspec | 29 + launchdarkly-server-sdk-redis-1.0-0.rockspec | 29 + launchdarkly-server-sdk-redis.c | 110 ++ launchdarkly-server-sdk-redis.lua | 63 - launchdarkly-server-sdk.c | 1218 ++++++++++++++++++ launchdarkly-server-sdk.lua | 671 ---------- test.lua | 75 +- 10 files changed, 1541 insertions(+), 758 deletions(-) create mode 100755 .ldrelease/build-c-server.sh create mode 100644 launchdarkly-server-sdk-1.0-0.rockspec create mode 100644 launchdarkly-server-sdk-redis-1.0-0.rockspec create mode 100644 launchdarkly-server-sdk-redis.c delete mode 100644 launchdarkly-server-sdk-redis.lua create mode 100644 launchdarkly-server-sdk.c delete mode 100644 launchdarkly-server-sdk.lua diff --git a/.circleci/config.yml b/.circleci/config.yml index 5bc7a42..8d7d012 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,10 +4,12 @@ workflows: version: 2 all: jobs: - - build-test-linux + - build-test-linux-luajit + - build-test-linux-lua53 + - build-test-linux-lua52 jobs: - build-test-linux: + build-test-linux-luajit: docker: - image: ubuntu:18.04 - image: redis @@ -15,22 +17,81 @@ jobs: - checkout - run: name: Prepare - command: ./.ldrelease/linux-prepare.sh + command: | + apt-get update -y + apt-get install -y luajit libluajit-5.1-dev lua-ldoc zip \ + ca-certificates curl zip lua-cjson libpcre3 libcurl4-openssl-dev \ + cmake libhiredis-dev git build-essential libpcre3-dev luarocks + + - run: + name: Build c-server-sdk + command: ./.ldrelease/build-c-server.sh + + - run: + name: Build lua-server-sdk + command: | + luarocks make launchdarkly-server-sdk-1.0-0.rockspec + luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec + + - run: + name: Run tests + command: | + luajit test.lua + + build-test-linux-lua53: + docker: + - image: ubuntu:18.04 + - image: redis + steps: + - checkout + - run: + name: Prepare + command: | + apt-get update -y + apt-get install -y lua5.3 liblua5.3-dev lua-ldoc zip \ + ca-certificates curl zip lua-cjson libpcre3 libcurl4-openssl-dev \ + cmake libhiredis-dev git build-essential libpcre3-dev luarocks + - run: - name: Build Doc - command: ./.ldrelease/linux-build-docs.sh + name: Build c-server-sdk + command: ./.ldrelease/build-c-server.sh + + - run: + name: Build lua-server-sdk + command: | + luarocks make launchdarkly-server-sdk-1.0-0.rockspec + luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec + + - run: + name: Run tests + command: | + lua5.3 test.lua + + build-test-linux-lua52: + docker: + - image: ubuntu:18.04 + - image: redis + steps: + - checkout + - run: + name: Prepare + command: | + apt-get update -y + apt-get install -y lua5.2 liblua5.2-dev lua-ldoc zip \ + ca-certificates curl zip lua-cjson libpcre3 libcurl4-openssl-dev \ + cmake libhiredis-dev git build-essential libpcre3-dev luarocks + - run: name: Build c-server-sdk + command: ./.ldrelease/build-c-server.sh + + - run: + name: Build lua-server-sdk command: | - git clone https://github.com/launchdarkly/c-server-sdk.git - cd c-server-sdk - mkdir build - cd build - cmake -D REDIS_STORE=ON -D BUILD_SHARED_LIBS=ON -D BUILD_TESTING=OFF .. - make + luarocks make launchdarkly-server-sdk-1.0-0.rockspec + luarocks make launchdarkly-server-sdk-redis-1.0-0.rockspec + - run: name: Run tests command: | - cp c-server-sdk/build/libldserverapi.so . - cp c-server-sdk/build/stores/redis/libldserverapi-redis.so . - LD_LIBRARY_PATH=. luajit test.lua + lua5.2 test.lua diff --git a/.ldrelease/build-c-server.sh b/.ldrelease/build-c-server.sh new file mode 100755 index 0000000..ffa9db3 --- /dev/null +++ b/.ldrelease/build-c-server.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +git clone https://github.com/launchdarkly/c-server-sdk.git +cd c-server-sdk +mkdir build +cd build +cmake -D BUILD_SHARED_LIBS=ON -D BUILD_TESTING=OFF -D REDIS_STORE=ON .. +make +make install diff --git a/.ldrelease/linux-build-docs.sh b/.ldrelease/linux-build-docs.sh index d753a86..d2c3f2f 100755 --- a/.ldrelease/linux-build-docs.sh +++ b/.ldrelease/linux-build-docs.sh @@ -7,8 +7,8 @@ set -e PROJECT_DIR=$(pwd) mkdir tmp -cp launchdarkly-server-sdk.lua tmp/ -cp launchdarkly-server-sdk-redis.lua tmp/ +cp launchdarkly-server-sdk.c tmp/ +cp launchdarkly-server-sdk-redis.c tmp/ ldoc tmp diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec new file mode 100644 index 0000000..e58ed20 --- /dev/null +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -0,0 +1,29 @@ +package = "launchdarkly-server-sdk" + +version = "1.0-0" + +source = { + url = "." -- not online yet! +} + +dependencies = { + "lua >= 5.1, <= 5.4", +} + +external_dependencies = { + LD = { + header = "launchdarkly/api.h" + } +} + +build = { + type = "builtin", + modules = { + ["launchdarkly_server_sdk"] = { + sources = { "launchdarkly-server-sdk.c" }, + incdirs = {"$(LD_INCDIR)"}, + libdirs = {"$(LD_LIBDIR)"}, + libraries = {"ldserverapi"} + } + } +} diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec new file mode 100644 index 0000000..1751204 --- /dev/null +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -0,0 +1,29 @@ +package = "launchdarkly-server-sdk-redis" + +version = "1.0-0" + +source = { + url = "." -- not online yet! +} + +dependencies = { + "lua >= 5.1, <= 5.4", +} + +external_dependencies = { + LDREDIS = { + header = "launchdarkly/store/redis.h" + } +} + +build = { + type = "builtin", + modules = { + ["launchdarkly_server_sdk_redis"] = { + sources = { "launchdarkly-server-sdk-redis.c" }, + incdirs = {"$(LDREDIS_INCDIR)"}, + libdirs = {"$(LDREDIS_LIBDIR)"}, + libraries = {"ldserverapi-redis"} + } + } +} diff --git a/launchdarkly-server-sdk-redis.c b/launchdarkly-server-sdk-redis.c new file mode 100644 index 0000000..606b63b --- /dev/null +++ b/launchdarkly-server-sdk-redis.c @@ -0,0 +1,110 @@ +/*** +Server-side SDK for LaunchDarkly Redis store. +@module launchdarkly-server-sdk-redis +*/ + +#include +#include +#include +#include +#include + +#include +#include + +/*** +Initialize a store backend +@function makeStore +@tparam table fields list of configuration options +@tparam[opt] string fields.host Hostname for Redis. +@tparam[opt] int fields.port Port for Redis. +@tparam[opt] string fields.prefix Redis key prefix for SDK values. +@tparam[opt] int fields.poolSize Number of Redis connections to maintain. +@return A fresh Redis store backend. +*/ +static int +LuaLDRedisMakeStore(lua_State *const l) +{ + struct LDRedisConfig *config; + struct LDStoreInterface *storeInterface; + + if (lua_gettop(l) != 1) { + return luaL_error(l, "expecting exactly 1 argument"); + } + + luaL_checktype(l, 1, LUA_TTABLE); + + config = LDRedisConfigNew(); + + lua_getfield(l, 1, "host"); + + if (lua_isstring(l, -1)) { + LDRedisConfigSetHost(config, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "prefix"); + + if (lua_isstring(l, -1)) { + LDRedisConfigSetPrefix(config, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "port"); + + if (lua_isnumber(l, -1)) { + LDRedisConfigSetPort(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, 1, "poolSize"); + + if (lua_isnumber(l, -1)) { + LDRedisConfigSetPoolSize(config, luaL_checkinteger(l, -1)); + } + + storeInterface = LDStoreInterfaceRedisNew(config); + + struct LDStoreInterface **i = + (struct LDStoreInterface **)lua_newuserdata(l, sizeof(storeInterface)); + + *i = storeInterface; + + luaL_getmetatable(l, "LaunchDarklyStoreInterface"); + lua_setmetatable(l, -2); + + return 1; +} + +static const struct luaL_Reg launchdarkly_functions[] = { + { "makeStore", LuaLDRedisMakeStore }, + { NULL, NULL } +}; + +#if !defined LUA_VERSION_NUM || LUA_VERSION_NUM==501 +/* +** Adapted from Lua 5.2.0 +*/ +static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { + luaL_checkstack(L, nup+1, "too many upvalues"); + for (; l->name != NULL; l++) { /* fill the table with given functions */ + int i; + lua_pushstring(L, l->name); + for (i = 0; i < nup; i++) /* copy upvalues to the top */ + lua_pushvalue(L, -(nup+1)); + lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ + lua_settable(L, -(nup + 3)); + } + lua_pop(L, nup); /* remove upvalues */ +} +#endif + +int +luaopen_launchdarkly_server_sdk_redis(lua_State *const l) +{ + #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 + luaL_newlib(l, launchdarkly_functions); + #else + luaL_register(l, "launchdarkly-server-sdk-redis", + launchdarkly_functions); + #endif + + return 1; +} diff --git a/launchdarkly-server-sdk-redis.lua b/launchdarkly-server-sdk-redis.lua deleted file mode 100644 index 3551f5f..0000000 --- a/launchdarkly-server-sdk-redis.lua +++ /dev/null @@ -1,63 +0,0 @@ ---- Server-side SDK for LaunchDarkly Redis store. --- @module launchdarkly-server-sdk-redis - -local ffi = require("ffi") - -ffi.cdef[[ - struct LDRedisConfig; - struct LDStoreInterface; - - struct LDRedisConfig *LDRedisConfigNew(); - - bool LDRedisConfigSetHost( - struct LDRedisConfig *const config, - const char *const host); - - bool LDRedisConfigSetPort( - struct LDRedisConfig *const config, - const unsigned short port); - - bool LDRedisConfigSetPrefix( - struct LDRedisConfig *const config, - const char *const prefix); - - bool LDRedisConfigSetPoolSize( - struct LDRedisConfig *const config, - const unsigned int poolSize); - - bool LDRedisConfigFree(struct LDRedisConfig *const config); - - struct LDStoreInterface *LDStoreInterfaceRedisNew( - struct LDRedisConfig *const config); -]] - -local so = ffi.load("ldserverapi-redis") - -local function applyWhenNotNil(subject, operation, value) - if value ~= nil and value ~= cjson.null then - operation(subject, value) - end -end - ---- Initialize a store backend --- @tparam table fields list of configuration options --- @tparam[opt] string fields.host Hostname for Redis. --- @tparam[opt] int fields.port Port for Redis. --- @tparam[opt] string fields.prefix Redis key prefix for SDK values. --- @tparam[opt] int fields.poolSize Number of Redis connections to maintain. --- @return A fresh Redis store backend. -local function makeStore(fields) - local config = so.LDRedisConfigNew() - - applyWhenNotNil(config, so.LDRedisConfigSetHost, fields["host"]) - applyWhenNotNil(config, so.LDRedisConfigSetPort, fields["port"]) - applyWhenNotNil(config, so.LDRedisConfigSetPrefix, fields["prefix"]) - applyWhenNotNil(config, so.LDRedisConfigSetPoolSize, fields["poolSize"]) - - return so.LDStoreInterfaceRedisNew(config) -end - ---- @export -return { - makeStore = makeStore -} diff --git a/launchdarkly-server-sdk.c b/launchdarkly-server-sdk.c new file mode 100644 index 0000000..a21186b --- /dev/null +++ b/launchdarkly-server-sdk.c @@ -0,0 +1,1218 @@ +/*** +Server-side SDK for LaunchDarkly. +@module launchdarkly-server-sdk +*/ + +#include +#include +#include +#include +#include + +#include + +#define SDKVersion "1.0.0-beta.2" + +static struct LDJSON * +LuaValueToJSON(lua_State *const l, const int i); + +static struct LDJSON * +LuaTableToJSON(lua_State *const l, const int i); + +static struct LDJSON * +LuaArrayToJSON(lua_State *const l, const int i); + +static void +LuaPushJSON(lua_State *const l, const struct LDJSON *const j); + +static int globalLoggingCallback; +static lua_State *globalLuaState; + +static void +logHandler(const LDLogLevel level, const char * const line) +{ + lua_rawgeti(globalLuaState, LUA_REGISTRYINDEX, globalLoggingCallback); + + lua_pushstring(globalLuaState, LDLogLevelToString(level)); + lua_pushstring(globalLuaState, line); + + lua_call(globalLuaState, 2, 0); +} + +static LDLogLevel +LuaStringToLogLevel(const char *const text) +{ + if (strcmp(text, "FATAL") == 0) { + return LD_LOG_FATAL; + } else if (strcmp(text, "CRITICAL") == 0) { + return LD_LOG_CRITICAL; + } else if (strcmp(text, "ERROR") == 0) { + return LD_LOG_ERROR; + } else if (strcmp(text, "WARNING") == 0) { + return LD_LOG_WARNING; + } else if (strcmp(text, "INFO") == 0) { + return LD_LOG_INFO; + } else if (strcmp(text, "DEBUG") == 0) { + return LD_LOG_DEBUG; + } else if (strcmp(text, "TRACE") == 0) { + return LD_LOG_TRACE; + } + + return LD_LOG_INFO; +} + +/*** +Set the global logger for all SDK operations. This function is not thread +safe, and if used should be done so before other operations. The default +log level is "INFO". +@function registerLogger +@tparam string logLevel The level to at. Available options are: +"FATAL", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE". +@tparam function cb The logging handler. Callback must be of the form +"function (logLevel, logLine) ... end". +*/ +static int +LuaLDRegisterLogger(lua_State *const l) +{ + if (lua_gettop(l) != 2) { + return luaL_error(l, "expecting exactly 2 arguments"); + } + + const char *const level = luaL_checkstring(l, 1); + + globalLuaState = l; + globalLoggingCallback = luaL_ref(l, LUA_REGISTRYINDEX); + + LDConfigureGlobalLogger(LuaStringToLogLevel(level), logHandler); + + return 0; +} + +static bool +isArray(lua_State *const l, const int i) +{ + lua_pushvalue(l, i); + + lua_pushnil(l); + + bool array = true; + + while (lua_next(l, -2) != 0) { + if (lua_type(l, -2) != LUA_TNUMBER) { + array = false; + } + + lua_pop(l, 1); + } + + lua_pop(l, 1); + + return array; +} + +static struct LDJSON * +LuaValueToJSON(lua_State *const l, const int i) +{ + struct LDJSON *result; + + switch (lua_type(l, i)) { + case LUA_TBOOLEAN: + result = LDNewBool(lua_toboolean(l, i)); + break; + case LUA_TNUMBER: + result = LDNewNumber(lua_tonumber(l, i)); + break; + case LUA_TSTRING: + result = LDNewText(lua_tostring(l, i)); + break; + case LUA_TTABLE: + if (isArray(l, i)) { + result = LuaArrayToJSON(l, i); + } else { + result = LuaTableToJSON(l, i); + } + break; + default: + result = LDNewNull(); + break; + } + + return result; +} + +static struct LDJSON * +LuaArrayToJSON(lua_State *const l, const int i) +{ + struct LDJSON *const result = LDNewArray(); + + lua_pushvalue(l, i); + + lua_pushnil(l); + + while (lua_next(l, -2) != 0) { + struct LDJSON *value = LuaValueToJSON(l, -1); + + LDArrayPush(result, value); + + lua_pop(l, 1); + } + + lua_pop(l, 1); + + return result; +} + +static struct LDJSON * +LuaTableToJSON(lua_State *const l, const int i) +{ + struct LDJSON *const result = LDNewObject(); + + lua_pushvalue(l, i); + + lua_pushnil(l); + + while (lua_next(l, -2) != 0) { + const char *const key = lua_tostring(l, -2); + struct LDJSON *const value = LuaValueToJSON(l, -1); + + LDObjectSetKey(result, key, value); + + lua_pop(l, 1); + } + + lua_pop(l, 1); + + return result; +} + +static void +LuaPushJSONObject(lua_State *const l, const struct LDJSON *const j) +{ + struct LDJSON *iter; + + lua_newtable(l); + + for (iter = LDGetIter(j); iter; iter = LDIterNext(iter)) { + LuaPushJSON(l, iter); + lua_setfield(l, -2, LDIterKey(iter)); + } +} + +static void +LuaPushJSONArray(lua_State *const l, const struct LDJSON *const j) +{ + struct LDJSON *iter; + + lua_newtable(l); + + int index = 1; + + for (iter = LDGetIter(j); iter; iter = LDIterNext(iter)) { + LuaPushJSON(l, iter); + lua_rawseti(l, -2, index); + index++; + } +} + +static void +LuaPushJSON(lua_State *const l, const struct LDJSON *const j) +{ + switch (LDJSONGetType(j)) { + case LDText: + lua_pushstring(l, LDGetText(j)); + break; + case LDBool: + lua_pushboolean(l, LDGetBool(j)); + break; + case LDNumber: + lua_pushnumber(l, LDGetNumber(j)); + break; + case LDObject: + LuaPushJSONObject(l, j); + break; + case LDArray: + LuaPushJSONArray(l, j); + break; + default: + lua_pushnil(l); + break; + } + + return; +} + +/*** +Create a new opaque user object. +@function makeUser +@tparam table fields list of user fields. +@tparam string fields.key The user's key +@tparam[opt] boolean fields.anonymous Mark the user as anonymous +@tparam[opt] string fields.ip Set the user's IP +@tparam[opt] string fields.firstName Set the user's first name +@tparam[opt] string fields.lastName Set the user's last name +@tparam[opt] string fields.email Set the user's email +@tparam[opt] string fields.name Set the user's name +@tparam[opt] string fields.avatar Set the user's avatar +@tparam[opt] string fields.country Set the user's country +@tparam[opt] string fields.secondary Set the user's secondary key +@tparam[opt] table fields.privateAttributeNames A list of attributes to +redact +@tparam[opt] table fields.custom Set the user's custom JSON +@return an opaque user object +*/ +static int +LuaLDUserNew(lua_State *const l) +{ + if (lua_gettop(l) != 1) { + return luaL_error(l, "expecting exactly 1 argument"); + } + + luaL_checktype(l, 1, LUA_TTABLE); + + lua_getfield(l, 1, "key"); + + const char *const key = luaL_checkstring(l, -1); + + struct LDUser *user = LDUserNew(key); + + lua_getfield(l, 1, "anonymous"); + + if (lua_isboolean(l, -1)) { + LDUserSetAnonymous(user, lua_toboolean(l, -1)); + } + + lua_getfield(l, 1, "ip"); + + if (lua_isstring(l, -1)) { + LDUserSetIP(user, luaL_checkstring(l,-1)); + }; + + lua_getfield(l, 1, "firstName"); + + if (lua_isstring(l, -1)) { + LDUserSetFirstName(user, luaL_checkstring(l,-1)); + } + + lua_getfield(l, 1, "lastName"); + + if (lua_isstring(l, -1)) { + LDUserSetLastName(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "email"); + + if (lua_isstring(l, -1)) { + LDUserSetEmail(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "name"); + + if (lua_isstring(l, -1)) { + LDUserSetName(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "avatar"); + + if (lua_isstring(l, -1)) { + LDUserSetAvatar(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "country"); + + if (lua_isstring(l, -1)) { + LDUserSetCountry(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "secondary"); + + if (lua_isstring(l, -1)) { + LDUserSetSecondary(user, luaL_checkstring(l, -1)); + } + + lua_getfield(l, 1, "custom"); + + if (lua_istable(l, -1)) { + LDUserSetCustom(user, LuaValueToJSON(l, -1)); + } + + lua_getfield(l, 1, "privateAttributeNames"); + + if (lua_istable(l, -1)) { + struct LDJSON *attrs, *iter; + attrs = LuaValueToJSON(l, -1); + + for (iter = LDGetIter(attrs); iter; iter = LDIterNext(iter)) { + LDUserAddPrivateAttribute(user, LDGetText(iter)); + } + + LDJSONFree(attrs); + } + + struct LDUser **u = (struct LDUser **)lua_newuserdata(l, sizeof(user)); + + *u = user; + + luaL_getmetatable(l, "LaunchDarklyUser"); + lua_setmetatable(l, -2); + + return 1; +} + +static int +LuaLDUserFree(lua_State *const l) +{ + struct LDUser **user; + + user = (struct LDUser **)luaL_checkudata(l, 1, "LaunchDarklyUser"); + + if (*user) { + LDUserFree(*user); + *user = NULL; + } + + return 0; +} + +static struct LDConfig * +makeConfig(lua_State *const l, const int i) +{ + struct LDConfig *config; + + luaL_checktype(l, i, LUA_TTABLE); + + lua_getfield(l, i, "key"); + + const char *const key = luaL_checkstring(l, -1); + + config = LDConfigNew(key); + + lua_getfield(l, i, "baseURI"); + + if (lua_isstring(l, -1)) { + LDConfigSetBaseURI(config, luaL_checkstring(l, -1)); + } + + lua_getfield(l, i, "streamURI"); + + if (lua_isstring(l, -1)) { + LDConfigSetStreamURI(config, luaL_checkstring(l, -1)); + } + + lua_getfield(l, i, "eventsURI"); + + if (lua_isstring(l, -1)) { + LDConfigSetEventsURI(config, luaL_checkstring(l, -1)); + } + + lua_getfield(l, i, "stream"); + + if (lua_isboolean(l, -1)) { + LDConfigSetStream(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "sendEvents"); + + if (lua_isstring(l, -1)) { + LDConfigSetSendEvents(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "eventsCapacity"); + + if (lua_isnumber(l, -1)) { + LDConfigSetEventsCapacity(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, i, "timeout"); + + if (lua_isnumber(l, -1)) { + LDConfigSetTimeout(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, i, "flushInterval"); + + if (lua_isnumber(l, -1)) { + LDConfigSetFlushInterval(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, i, "pollInterval"); + + if (lua_isnumber(l, -1)) { + LDConfigSetPollInterval(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, i, "offline"); + + if (lua_isboolean(l, -1)) { + LDConfigSetOffline(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "useLDD"); + + if (lua_isboolean(l, -1)) { + LDConfigSetUseLDD(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "inlineUsersInEvents"); + + if (lua_isboolean(l, -1)) { + LDConfigInlineUsersInEvents(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "allAttributesPrivate"); + + if (lua_isboolean(l, -1)) { + LDConfigSetAllAttributesPrivate(config, lua_toboolean(l, -1)); + } + + lua_getfield(l, i, "userKeysCapacity"); + + if (lua_isnumber(l, -1)) { + LDConfigSetUserKeysCapacity(config, luaL_checkinteger(l, -1)); + } + + lua_getfield(l, i, "featureStoreBackend"); + + if (lua_isuserdata(l, -1)) { + struct LDStoreInterface **storeInterface; + + storeInterface = (struct LDStoreInterface **) + luaL_checkudata(l, -1, "LaunchDarklyStoreInterface"); + + LDConfigSetFeatureStoreBackend(config, *storeInterface); + } + + lua_getfield(l, 1, "privateAttributeNames"); + + if (lua_istable(l, -1)) { + struct LDJSON *attrs, *iter; + attrs = LuaValueToJSON(l, -1); + + for (iter = LDGetIter(attrs); iter; iter = LDIterNext(iter)) { + LDConfigAddPrivateAttribute(config, LDGetText(iter)); + } + + LDJSONFree(attrs); + } + + LDConfigSetWrapperInfo(config, "lua-server-sdk", SDKVersion); + + return config; +} + +/*** +Initialize a new client, and connect to LaunchDarkly. +@function makeClient +@tparam table config list of configuration options +@tparam string config.key Environment SDK key +@tparam[opt] string config.baseURI Set the base URI for connecting to +LaunchDarkly. You probably don't need to set this unless instructed by +LaunchDarkly. +@tparam[opt] string config.streamURI Set the streaming URI for connecting to +LaunchDarkly. You probably don't need to set this unless instructed by +LaunchDarkly. +@tparam[opt] string config.eventsURI Set the events URI for connecting to +LaunchDarkly. You probably don't need to set this unless instructed by +LaunchDarkly. +@tparam[opt] boolean config.stream Enables or disables real-time streaming +flag updates. When set to false, an efficient caching polling mechanism is +used. We do not recommend disabling streaming unless you have been instructed +to do so by LaunchDarkly support. Defaults to true. +@tparam[opt] string config.sendEvents Sets whether to send analytics events +back to LaunchDarkly. By default, the client will send events. This differs +from Offline in that it only affects sending events, not streaming or +polling. +@tparam[opt] int config.eventsCapacity The capacity of the events buffer. +The client buffers up to this many events in memory before flushing. If the +capacity is exceeded before the buffer is flushed, events will be discarded. +@tparam[opt] int config.timeout The connection timeout to use when making +requests to LaunchDarkly. +@tparam[opt] int config.flushInterval he time between flushes of the event +buffer. Decreasing the flush interval means that the event buffer is less +likely to reach capacity. +@tparam[opt] int config.pollInterval The polling interval +(when streaming is disabled) in milliseconds. +@tparam[opt] boolean config.offline Sets whether this client is offline. +An offline client will not make any network connections to LaunchDarkly, +and will return default values for all feature flags. +@tparam[opt] boolean config.allAttributesPrivate Sets whether or not all user +attributes (other than the key) should be hidden from LaunchDarkly. If this +is true, all user attribute values will be private, not just the attributes +specified in PrivateAttributeNames. +@tparam[opt] boolean config.inlineUsersInEvents Set to true if you need to +see the full user details in every analytics event. +@tparam[opt] int config.userKeysCapacity The number of user keys that the +event processor can remember at an one time, so that duplicate user details +will not be sent in analytics. +@tparam[opt] int config.userKeysFlushInterval The interval at which the event +processor will reset its set of known user keys, in milliseconds. +@tparam[opt] table config.privateAttributeNames Marks a set of user attribute +names private. Any users sent to LaunchDarkly with this configuration active +will have attributes with these names removed. +@param[opt] backend config.featureStoreBackend Persistent feature store +backend. +@tparam int timeoutMilliseconds How long to wait for flags to +download. If the timeout is reached a non fully initialized client will be +returned. +@return A fresh client. +*/ +static int +LuaLDClientInit(lua_State *const l) +{ + struct LDClient *client; + struct LDConfig *config; + unsigned int timeout; + + if (lua_gettop(l) != 2) { + return luaL_error(l, "expecting exactly 2 arguments"); + } + + config = makeConfig(l, 1); + + timeout = luaL_checkinteger(l, 2); + + client = LDClientInit(config, timeout); + + struct LDClient **c = + (struct LDClient **)lua_newuserdata(l, sizeof(client)); + + *c = client; + + luaL_getmetatable(l, "LaunchDarklyClient"); + lua_setmetatable(l, -2); + + return 1; +} + +static int +LuaLDClientClose(lua_State *const l) +{ + struct LDClient **client; + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + if (*client) { + LDClientClose(*client); + *client = NULL; + } + + return 0; +} + +static void +LuaPushDetails(lua_State *const l, struct LDDetails *const details, + struct LDJSON *const value) +{ + struct LDJSON *reason; + + reason = LDReasonToJSON(details); + + lua_newtable(l); + + LuaPushJSON(l, reason); + lua_setfield(l, -2, "reason"); + + if (details->hasVariation) { + lua_pushnumber(l, details->variationIndex); + lua_setfield(l, -2, "variationIndex"); + } + + LuaPushJSON(l, value); + lua_setfield(l, -2, "value"); + + LDDetailsClear(details); + LDJSONFree(value); + LDJSONFree(reason); +} + +/** +Evaluate a boolean flag +@class function +@name boolVariation +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam boolean fallback The value to return on error +@treturn boolean The evaluation result, or the fallback value +*/ +static int +LuaLDClientBoolVariation(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const int fallback = lua_toboolean(l, 4); + + const LDBoolean result = + LDBoolVariation(*client, *user, key, fallback, NULL); + + lua_pushboolean(l, result); + + return 1; +} + +/*** +Evaluate a boolean flag and return an explanation +@class function +@name boolVariationDetail +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam boolean fallback The value to return on error +@treturn table The evaluation explanation +*/ +static int +LuaLDClientBoolVariationDetail(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDDetails details; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + LDDetailsInit(&details); + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const int fallback = lua_toboolean(l, 4); + + const LDBoolean result = + LDBoolVariation(*client, *user, key, fallback, &details); + + LuaPushDetails(l, &details, LDNewBool(result)); + + return 1; +} + +/*** +Evaluate an integer flag +@class function +@name intVariation +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam int fallback The value to return on error +@treturn int The evaluation result, or the fallback value +*/ +static int +LuaLDClientIntVariation(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const int fallback = luaL_checkinteger(l, 4); + + const int result = LDIntVariation(*client, *user, key, fallback, NULL); + + lua_pushnumber(l, result); + + return 1; +} + +/*** +Evaluate an integer flag and return an explanation +@class function +@name intVariationDetail +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam int fallback The value to return on error +@treturn table The evaluation explanation +*/ +static int +LuaLDClientIntVariationDetail(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDDetails details; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + LDDetailsInit(&details); + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const int fallback = luaL_checkinteger(l, 4); + + const int result = LDIntVariation(*client, *user, key, fallback, &details); + + LuaPushDetails(l, &details, LDNewNumber(result)); + + return 1; +} + +/*** +Evaluate a double flag +@class function +@name doubleVariation +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam number fallback The value to return on error +@treturn double The evaluation result, or the fallback value +*/ +static int +LuaLDClientDoubleVariation(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const double fallback = lua_tonumber(l, 4); + + const double result = + LDDoubleVariation(*client, *user, key, fallback, NULL); + + lua_pushnumber(l, result); + + return 1; +} + +/*** +Evaluate a double flag and return an explanation +@class function +@name doubleVariationDetail +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam number fallback The value to return on error +@treturn table The evaluation explanation +*/ +static int +LuaLDClientDoubleVariationDetail(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDDetails details; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + LDDetailsInit(&details); + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const double fallback = lua_tonumber(l, 4); + + const double result = + LDDoubleVariation(*client, *user, key, fallback, &details); + + LuaPushDetails(l, &details, LDNewNumber(result)); + + return 1; +} + +/*** +Evaluate a string flag +@class function +@name stringVariation +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam string fallback The value to return on error +@treturn string The evaluation result, or the fallback value +*/ +static int +LuaLDClientStringVariation(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const char *const fallback = luaL_checkstring(l, 4); + + char *const result = + LDStringVariation(*client, *user, key, fallback, NULL); + + lua_pushstring(l, result); + + LDFree(result); + + return 1; +} + +/*** +Evaluate a string flag and return an explanation +@class function +@name stringVariationDetail +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam string fallback The value to return on error +@treturn table The evaluation explanation +*/ +static int +LuaLDClientStringVariationDetail(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDDetails details; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + LDDetailsInit(&details); + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + const char *const fallback = luaL_checkstring(l, 4); + + char *const result = + LDStringVariation(*client, *user, key, fallback, &details); + + LuaPushDetails(l, &details, LDNewText(result)); + + LDFree(result); + + return 1; +} + +/*** +Evaluate a json flag +@class function +@name jsonVariation +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam table fallback The value to return on error +@treturn table The evaluation result, or the fallback value +*/ +static int +LuaLDClientJSONVariation(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDJSON *fallback, *result; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + fallback = LuaValueToJSON(l, 4); + + result = LDJSONVariation(*client, *user, key, fallback, NULL); + + LuaPushJSON(l, result); + + LDJSONFree(fallback); + LDJSONFree(result); + + return 1; +} + +/*** +Evaluate a json flag and return an explanation +@class function +@name jsonVariationDetail +@tparam user user An opaque user object from @{makeUser} +@tparam string key The key of the flag to evaluate. +@tparam table fallback The value to return on error +@treturn table The evaluation explanation +*/ +static int +LuaLDClientJSONVariationDetail(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDJSON *fallback, *result; + struct LDDetails details; + + if (lua_gettop(l) != 4) { + return luaL_error(l, "expecting exactly 4 arguments"); + } + + LDDetailsInit(&details); + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + const char *const key = luaL_checkstring(l, 3); + + fallback = LuaValueToJSON(l, 4); + + result = LDJSONVariation(*client, *user, key, fallback, &details); + + LuaPushDetails(l, &details, result); + + LDJSONFree(fallback); + + return 1; +} + +/*** +Immediately flushes queued events. +@function flush +@treturn nil +*/ +static int +LuaLDClientFlush(lua_State *const l) +{ + struct LDClient **client; + + if (lua_gettop(l) != 1) { + return luaL_error(l, "expecting exactly 1 argument"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + LDClientFlush(*client); + + return 0; +} + +/*** +Reports that a user has performed an event. Custom data, and a metric +can be attached to the event as JSON. +@function track +@tparam string key The name of the event +@tparam user user An opaque user object from @{makeUser} +@tparam[opt] table data A value to be associated with an event +@tparam[optchain] number metric A value to be associated with an event +@treturn nil +*/ +static int +LuaLDClientTrack(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDJSON *value; + + if (lua_gettop(l) < 3 || lua_gettop(l) > 5) { + return luaL_error(l, "expecting 3-5 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + const char *const key = luaL_checkstring(l, 2); + + user = (struct LDUser **)luaL_checkudata(l, 3, "LaunchDarklyUser"); + + if (lua_isnil(l, 4)) { + value = NULL; + } else { + value = LuaValueToJSON(l, 4); + } + + if (lua_gettop(l) == 5 && lua_isnumber(l, 5)) { + const double metric = luaL_checknumber(l, 5); + + LDClientTrackMetric(*client, key, *user, value, metric); + } else { + LDClientTrack(*client, key, *user, value); + } + + return 0; +} + +/*** +Check if a client has been fully initialized. This may be useful if the +initialization timeout was reached. +@function isInitialized +@treturn boolean true if fully initialized +*/ +static int +LuaLDClientIsInitialized(lua_State *const l) +{ + struct LDClient **client; + + if (lua_gettop(l) != 1) { + return luaL_error(l, "expecting exactly 1 argument"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + lua_pushboolean(l, LDClientIsInitialized(*client)); + + return 1; +} + +/*** +Generates an identify event for a user. +@function identify +@tparam user user An opaque user object from @{makeUser} +@treturn nil +*/ +static int +LuaLDClientIdentify(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + + if (lua_gettop(l) != 2) { + return luaL_error(l, "expecting exactly 2 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + LDClientIdentify(*client, *user); + + return 0; +} + +/*** +Returns a map from feature flag keys to values for a given user. +This does not send analytics events back to LaunchDarkly. +@function allFlags +@tparam user user An opaque user object from @{makeUser} +@treturn table +*/ +static int +LuaLDClientAllFlags(lua_State *const l) +{ + struct LDClient **client; + struct LDUser **user; + struct LDJSON *result; + + if (lua_gettop(l) != 2) { + return luaL_error(l, "expecting exactly 2 arguments"); + } + + client = (struct LDClient **)luaL_checkudata(l, 1, "LaunchDarklyClient"); + + user = (struct LDUser **)luaL_checkudata(l, 2, "LaunchDarklyUser"); + + result = LDAllFlags(*client, *user); + + LuaPushJSON(l, result); + + return 1; +} + +static const struct luaL_Reg launchdarkly_functions[] = { + { "clientInit", LuaLDClientInit }, + { "makeUser", LuaLDUserNew }, + { "registerLogger", LuaLDRegisterLogger }, + { NULL, NULL } +}; + +static const struct luaL_Reg launchdarkly_client_methods[] = { + { "boolVariation", LuaLDClientBoolVariation }, + { "boolVariationDetail", LuaLDClientBoolVariationDetail }, + { "intVariation", LuaLDClientIntVariation }, + { "intVariationDetail", LuaLDClientIntVariationDetail }, + { "doubleVariation", LuaLDClientDoubleVariation }, + { "doubleVariationDetail", LuaLDClientDoubleVariationDetail }, + { "stringVariation", LuaLDClientStringVariation }, + { "stringVariationDetail", LuaLDClientStringVariationDetail }, + { "jsonVariation", LuaLDClientJSONVariation }, + { "jsonVariationDetail", LuaLDClientJSONVariationDetail }, + { "flush", LuaLDClientFlush }, + { "track", LuaLDClientTrack }, + { "allFlags", LuaLDClientAllFlags }, + { "isInitialized", LuaLDClientIsInitialized }, + { "identify", LuaLDClientIdentify }, + { "__gc", LuaLDClientClose }, + { NULL, NULL } +}; + +static const struct luaL_Reg launchdarkly_user_methods[] = { + { "__gc", LuaLDUserFree }, + { NULL, NULL } +}; + +static const struct luaL_Reg launchdarkly_store_methods[] = { + { NULL, NULL } +}; + +#if !defined LUA_VERSION_NUM || LUA_VERSION_NUM==501 +/* +** Adapted from Lua 5.2.0 +*/ +static void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) { + luaL_checkstack(L, nup+1, "too many upvalues"); + for (; l->name != NULL; l++) { /* fill the table with given functions */ + int i; + lua_pushstring(L, l->name); + for (i = 0; i < nup; i++) /* copy upvalues to the top */ + lua_pushvalue(L, -(nup+1)); + lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ + lua_settable(L, -(nup + 3)); + } + lua_pop(L, nup); /* remove upvalues */ +} +#endif + +int +luaopen_launchdarkly_server_sdk(lua_State *const l) +{ + luaL_newmetatable(l, "LaunchDarklyClient"); + lua_pushvalue(l, -1); + lua_setfield(l, -2, "__index"); + luaL_setfuncs(l, launchdarkly_client_methods, 0); + + luaL_newmetatable(l, "LaunchDarklyUser"); + lua_pushvalue(l, -1); + lua_setfield(l, -2, "__index"); + luaL_setfuncs(l, launchdarkly_user_methods, 0); + + luaL_newmetatable(l, "LaunchDarklyStoreInterface"); + lua_pushvalue(l, -1); + lua_setfield(l, -2, "__index"); + luaL_setfuncs(l, launchdarkly_store_methods, 0); + + #if LUA_VERSION_NUM == 503 || LUA_VERSION_NUM == 502 + luaL_newlib(l, launchdarkly_functions); + #else + luaL_register(l, "launchdarkly-server-sdk", launchdarkly_functions); + #endif + + return 1; +} diff --git a/launchdarkly-server-sdk.lua b/launchdarkly-server-sdk.lua deleted file mode 100644 index e2bb656..0000000 --- a/launchdarkly-server-sdk.lua +++ /dev/null @@ -1,671 +0,0 @@ ---- Server-side SDK for LaunchDarkly. --- @module launchdarkly-server-sdk - -local ffi = require("ffi") -local cjson = require("cjson") - -ffi.cdef[[ - struct LDJSON; - typedef enum { - LDNull = 0, - LDText, - LDNumber, - LDBool, - LDObject, - LDArray - } LDJSONType; - struct LDJSON * LDNewNull(); - struct LDJSON * LDNewBool(const bool boolean); - struct LDJSON * LDNewNumber(const double number); - struct LDJSON * LDNewText(const char *const text); - struct LDJSON * LDNewObject(); - struct LDJSON * LDNewArray(); - bool LDSetNumber(struct LDJSON *const node, const double number); - void LDJSONFree(struct LDJSON *const json); - struct LDJSON * LDJSONDuplicate(const struct LDJSON *const json); - LDJSONType LDJSONGetType(const struct LDJSON *const json); - bool LDJSONCompare(const struct LDJSON *const left, - const struct LDJSON *const right); - bool LDGetBool(const struct LDJSON *const node); - double LDGetNumber(const struct LDJSON *const node); - const char * LDGetText(const struct LDJSON *const node); - struct LDJSON * LDIterNext(const struct LDJSON *const iter); - struct LDJSON * LDGetIter(const struct LDJSON *const collection); - const char * LDIterKey(const struct LDJSON *const iter); - unsigned int LDCollectionGetSize( - const struct LDJSON *const collection); - struct LDJSON * LDCollectionDetachIter( - struct LDJSON *const collection, struct LDJSON *const iter); - struct LDJSON * LDArrayLookup(const struct LDJSON *const array, - const unsigned int index); - bool LDArrayPush(struct LDJSON *const array, - struct LDJSON *const item); - bool LDArrayAppend(struct LDJSON *const prefix, - const struct LDJSON *const suffix); - struct LDJSON * LDObjectLookup(const struct LDJSON *const object, - const char *const key); - bool LDObjectSetKey(struct LDJSON *const object, - const char *const key, struct LDJSON *const item); - void LDObjectDeleteKey(struct LDJSON *const object, - const char *const key); - struct LDJSON * LDObjectDetachKey(struct LDJSON *const object, - const char *const key); - bool LDObjectMerge(struct LDJSON *const to, - const struct LDJSON *const from); - char * LDJSONSerialize(const struct LDJSON *const json); - struct LDJSON * LDJSONDeserialize(const char *const text); - struct LDStoreInterface; struct LDConfig; - struct LDConfig * LDConfigNew(const char *const key); - void LDConfigFree(struct LDConfig *const config); - bool LDConfigSetBaseURI(struct LDConfig *const config, - const char *const baseURI); - bool LDConfigSetStreamURI(struct LDConfig *const config, - const char *const streamURI); - bool LDConfigSetEventsURI(struct LDConfig *const config, - const char *const eventsURI); - void LDConfigSetStream(struct LDConfig *const config, - const bool stream); - void LDConfigSetSendEvents(struct LDConfig *const config, - const bool sendEvents); - void LDConfigSetEventsCapacity(struct LDConfig *const config, - const unsigned int eventsCapacity); - void LDConfigSetTimeout(struct LDConfig *const config, - const unsigned int milliseconds); - void LDConfigSetFlushInterval(struct LDConfig *const config, - const unsigned int milliseconds); - void LDConfigSetPollInterval(struct LDConfig *const config, - const unsigned int milliseconds); - void LDConfigSetOffline(struct LDConfig *const config, - const bool offline); - void LDConfigSetUseLDD(struct LDConfig *const config, - const bool useLDD); - void LDConfigSetAllAttributesPrivate(struct LDConfig *const config, - const bool allAttributesPrivate); - void LDConfigInlineUsersInEvents(struct LDConfig *const config, - const bool inlineUsersInEvents); - void LDConfigSetUserKeysCapacity(struct LDConfig *const config, - const unsigned int userKeysCapacity); - void LDConfigSetUserKeysFlushInterval(struct LDConfig *const config, - const unsigned int milliseconds); - bool LDConfigAddPrivateAttribute(struct LDConfig *const config, - const char *const attribute); - void LDConfigSetFeatureStoreBackend(struct LDConfig *const config, - struct LDStoreInterface *const backend); - bool LDConfigSetWrapperInfo(struct LDConfig *const config, - const char *const wrapperName, const char *const wrapperVersion); - struct LDUser * LDUserNew(const char *const userkey); - void LDUserFree(struct LDUser *const user); - void LDUserSetAnonymous(struct LDUser *const user, const bool anon); - bool LDUserSetIP(struct LDUser *const user, const char *const ip); - bool LDUserSetFirstName(struct LDUser *const user, - const char *const firstName); - bool LDUserSetLastName(struct LDUser *const user, - const char *const lastName); - bool LDUserSetEmail(struct LDUser *const user, - const char *const email); - bool LDUserSetName(struct LDUser *const user, - const char *const name); - bool LDUserSetAvatar(struct LDUser *const user, - const char *const avatar); - bool LDUserSetCountry(struct LDUser *const user, - const char *const country); - bool LDUserSetSecondary(struct LDUser *const user, - const char *const secondary); - void LDUserSetCustom(struct LDUser *const user, - struct LDJSON *const custom); - bool LDUserAddPrivateAttribute(struct LDUser *const user, - const char *const attribute); - struct LDClient * LDClientInit(struct LDConfig *const config, - const unsigned int maxwaitmilli); - void LDClientClose(struct LDClient *const client); - bool LDClientIsInitialized(struct LDClient *const client); - bool LDClientTrack(struct LDClient *const client, - const char *const key, const struct LDUser *const user, - struct LDJSON *const data); - bool LDClientTrackMetric(struct LDClient *const client, - const char *const key, const struct LDUser *const user, - struct LDJSON *const data, const double metric); - bool LDClientIdentify(struct LDClient *const client, - const struct LDUser *const user); - bool LDClientIsOffline(struct LDClient *const client); - void LDClientFlush(struct LDClient *const client); - void * LDAlloc(const size_t bytes); - void LDFree(void *const buffer); - char * LDStrDup(const char *const string); - void * LDRealloc(void *const buffer, const size_t bytes); - void * LDCalloc(const size_t nmemb, const size_t size); - char * LDStrNDup(const char *const str, const size_t n); - void LDSetMemoryRoutines(void *(*const newMalloc)(const size_t), - void (*const newFree)(void *const), - void *(*const newRealloc)(void *const, const size_t), - char *(*const newStrDup)(const char *const), - void *(*const newCalloc)(const size_t, const size_t), - char *(*const newStrNDup)(const char *const, const size_t)); - void LDGlobalInit(); - typedef enum { - LD_LOG_FATAL = 0, - LD_LOG_CRITICAL, - LD_LOG_ERROR, - LD_LOG_WARNING, - LD_LOG_INFO, - LD_LOG_DEBUG, - LD_LOG_TRACE - } LDLogLevel; - void LDi_log(const LDLogLevel level, const char *const format, ...); - void LDBasicLogger(const LDLogLevel level, const char *const text); - void LDConfigureGlobalLogger(const LDLogLevel level, - void (*logger)(const LDLogLevel level, const char *const text)); - const char * LDLogLevelToString(const LDLogLevel level); - enum LDEvalReason { - LD_UNKNOWN = 0, - LD_ERROR, - LD_OFF, - LD_PREREQUISITE_FAILED, - LD_TARGET_MATCH, - LD_RULE_MATCH, - LD_FALLTHROUGH - }; - enum LDEvalErrorKind { - LD_CLIENT_NOT_READY, - LD_NULL_KEY, - LD_STORE_ERROR, - LD_FLAG_NOT_FOUND, - LD_USER_NOT_SPECIFIED, - LD_MALFORMED_FLAG, - LD_WRONG_TYPE, - LD_OOM - }; - struct LDDetailsRule { - unsigned int ruleIndex; - char *id; - }; - struct LDDetails { - unsigned int variationIndex; - bool hasVariation; - enum LDEvalReason reason; - union { - enum LDEvalErrorKind errorKind; - char *prerequisiteKey; - struct LDDetailsRule rule; - } extra; - }; - void LDDetailsInit(struct LDDetails *const details); - void LDDetailsClear(struct LDDetails *const details); - const char * LDEvalReasonKindToString(const enum LDEvalReason kind); - const char * LDEvalErrorKindToString( - const enum LDEvalErrorKind kind); - struct LDJSON * LDReasonToJSON( - const struct LDDetails *const details); - bool LDBoolVariation(struct LDClient *const client, - struct LDUser *const user, const char *const key, const bool fallback, - struct LDDetails *const details); - int LDIntVariation(struct LDClient *const client, - struct LDUser *const user, const char *const key, const int fallback, - struct LDDetails *const details); - double LDDoubleVariation(struct LDClient *const client, - struct LDUser *const user, const char *const key, const double fallback, - struct LDDetails *const details); - char * LDStringVariation(struct LDClient *const client, - struct LDUser *const user, const char *const key, - const char* const fallback, struct LDDetails *const details); - struct LDJSON * LDJSONVariation(struct LDClient *const client, - struct LDUser *const user, const char *const key, - const struct LDJSON *const fallback, struct LDDetails *const details); - struct LDJSON * LDAllFlags(struct LDClient *const client, - struct LDUser *const user); -]] - -local SDKVersion = "1.0.0-beta.2" - -local so = ffi.load("ldserverapi") - -local function applyWhenNotNil(subject, operation, value) - if value ~= nil and value ~= cjson.null then - operation(subject, value) - end -end - -local function toLaunchDarklyJSON(x) - return ffi.gc(so.LDJSONDeserialize(cjson.encode(x)), so.LDJSONFree) -end - -local function toLaunchDarklyJSONTransfer(x) - return so.LDJSONDeserialize(cjson.encode(x)) -end - -local function fromLaunchDarklyJSON(x) - local raw = so.LDJSONSerialize(x) - local native = ffi.string(raw) - so.LDFree(raw) - return cjson.decode(native) -end - ---- Details associated with an evaluation --- @name Details --- @class table --- @tfield[opt] int variationIndex The index of the returned value within the --- flag's list of variations. --- @field value The resulting value of an evaluation --- @tfield table reason The reason a specific value was returned --- @tfield string reason.kind The kind of reason --- @tfield[opt] string reason.errorKind If the kind is LD_ERROR, this contains --- the error string. --- @tfield[opt] string reason.ruleId If the kind is LD_RULE_MATCH this contains --- the id of the rule. --- @tfield[opt] int reason.ruleIndex If the kind is LD_RULE_MATCH this contains --- the index of the rule. --- @tfield[opt] string reason.prerequisiteKey If the kind is --- LD_PREREQUISITE_FAILED this contains the key of the failed prerequisite. - -local function convertDetails(cDetails, value) - local details = {} - local cReasonJSON = so.LDReasonToJSON(cDetails) - details.reason = fromLaunchDarklyJSON(cReasonJSON) - - if cDetails.hasVariation then - details.variationIndex = cDetails.variationIndex - end - - details.value = value - return details -end - -local function genericVariationDetail(client, user, key, fallback, variation, valueConverter) - local cDetails = ffi.new("struct LDDetails") - so.LDDetailsInit(cDetails) - local value = variation(client, user, key, fallback, cDetails) - if valueConverter ~= nil then - value = valueConverter(value) - end - local details = convertDetails(cDetails, value) - so.LDDetailsClear(cDetails) - return details -end - -local function stringToLogLevel(level) - local translation = { - ["FATAL"] = so.LD_LOG_FATAL, - ["CRITICAL"] = so.LD_LOG_CRITICAL, - ["ERROR"] = so.LD_LOG_ERROR, - ["WARNING"] = so.LD_LOG_WARNING, - ["INFO"] = so.LD_LOG_INFO, - ["DEBUG"] = so.LD_LOG_DEBUG, - ["TRACE"] = so.LD_LOG_TRACE - } - - local lookup = translation[level] - - if lookup == nil then - return so.LD_LOG_INFO - else - return lookup - end -end - ---- Set the global logger for all SDK operations. This function is not thread --- safe, and if used should be done so before other operations. The default --- log level is "INFO". --- @tparam string logLevel The level to at. Available options are: --- "FATAL", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "TRACE". --- @tparam function cb The logging handler. Callback must be of the form --- "function (logLevel, logLine) ... end". -local function registerLogger(logLevel, cb) - so.LDConfigureGlobalLogger(stringToLogLevel(logLevel), - function(logLevel, line) - cb(ffi.string(so.LDLogLevelToString(logLevel)), ffi.string(line)) - end - ) -end - ---- make a config -local function makeConfig(fields) - local config = so.LDConfigNew(fields["key"]) - - applyWhenNotNil(config, so.LDConfigSetBaseURI, fields["baseURI"]) - applyWhenNotNil(config, so.LDConfigSetStreamURI, fields["streamURI"]) - applyWhenNotNil(config, so.LDConfigSetEventsURI, fields["eventsURI"]) - applyWhenNotNil(config, so.LDConfigSetStream, fields["stream"]) - applyWhenNotNil(config, so.LDConfigSetSendEvents, fields["sendEvents"]) - applyWhenNotNil(config, so.LDConfigSetEventsCapacity, fields["eventsCapacity"]) - applyWhenNotNil(config, so.LDConfigSetTimeout, fields["timeout"]) - applyWhenNotNil(config, so.LDConfigSetFlushInterval, fields["flushInterval"]) - applyWhenNotNil(config, so.LDConfigSetPollInterval, fields["pollInterval"]) - applyWhenNotNil(config, so.LDConfigSetOffline, fields["offline"]) - applyWhenNotNil(config, so.LDConfigSetAllAttributesPrivate, fields["allAttributesPrivate"]) - applyWhenNotNil(config, so.LDConfigInlineUsersInEvents, fields["inlineUsersInEvents"]) - applyWhenNotNil(config, so.LDConfigSetUserKeysCapacity, fields["userKeysCapacity"]) - applyWhenNotNil(config, so.LDConfigSetUserKeysFlushInterval, fields["userKeysFlushInterval"]) - applyWhenNotNil(config, so.LDConfigSetFeatureStoreBackend, fields["featureStoreBackend"]) - - so.LDConfigSetWrapperInfo(config, "lua-server-sdk", SDKVersion) - - local names = fields["privateAttributeNames"] - - if names ~= nil and names ~= cjson.null then - for _, v in ipairs(names) do - so.LDConfigAddPrivateAttribute(config, v) - end - end - - return config -end - ---- Create a new opaque user object. --- @tparam table fields list of user fields. --- @tparam string fields.key The user's key --- @tparam[opt] boolean fields.anonymous Mark the user as anonymous --- @tparam[opt] string fields.ip Set the user's IP --- @tparam[opt] string fields.firstName Set the user's first name --- @tparam[opt] string fields.lastName Set the user's last name --- @tparam[opt] string fields.email Set the user's email --- @tparam[opt] string fields.name Set the user's name --- @tparam[opt] string fields.avatar Set the user's avatar --- @tparam[opt] string fields.country Set the user's country --- @tparam[opt] string fields.secondary Set the user's secondary key --- @tparam[opt] table fields.privateAttributeNames A list of attributes to --- redact --- @tparam[opt] table fields.custom Set the user's custom JSON --- @return an opaque user object -local function makeUser(fields) - local user = ffi.gc(so.LDUserNew(fields["key"]), so.LDUserFree) - - applyWhenNotNil(user, so.LDUserSetAnonymous, fields["anonymous"]) - applyWhenNotNil(user, so.LDUserSetIP, fields["ip"]) - applyWhenNotNil(user, so.LDUserSetFirstName, fields["firstName"]) - applyWhenNotNil(user, so.LDUserSetLastName, fields["lastName"]) - applyWhenNotNil(user, so.LDUserSetEmail, fields["email"]) - applyWhenNotNil(user, so.LDUserSetName, fields["name"]) - applyWhenNotNil(user, so.LDUserSetAvatar, fields["avatar"]) - applyWhenNotNil(user, so.LDUserSetCountry, fields["country"]) - applyWhenNotNil(user, so.LDUserSetSecondary, fields["secondary"]) - - if fields["custom"] ~= nil then - so.LDUserSetCustom(user, toLaunchDarklyJSONTransfer(fields["custom"])) - end - - local names = fields["privateAttributeNames"] - - if names ~= nil and names ~= cjson.null then - for _, v in ipairs(names) do - so.LDUserAddPrivateAttribute(user, v) - end - end - - return user -end - ---- Initialize a new client, and connect to LaunchDarkly. --- @tparam table config list of configuration options --- @tparam string config.key Environment SDK key --- @tparam[opt] string config.baseURI Set the base URI for connecting to --- LaunchDarkly. You probably don't need to set this unless instructed by --- LaunchDarkly. --- @tparam[opt] string config.streamURI Set the streaming URI for connecting to --- LaunchDarkly. You probably don't need to set this unless instructed by --- LaunchDarkly. --- @tparam[opt] string config.eventsURI Set the events URI for connecting to --- LaunchDarkly. You probably don't need to set this unless instructed by --- LaunchDarkly. --- @tparam[opt] boolean config.stream Enables or disables real-time streaming --- flag updates. When set to false, an efficient caching polling mechanism is --- used. We do not recommend disabling streaming unless you have been instructed --- to do so by LaunchDarkly support. Defaults to true. --- @tparam[opt] string config.sendEvents Sets whether to send analytics events --- back to LaunchDarkly. By default, the client will send events. This differs --- from Offline in that it only affects sending events, not streaming or --- polling. --- @tparam[opt] int config.eventsCapacity The capacity of the events buffer. --- The client buffers up to this many events in memory before flushing. If the --- capacity is exceeded before the buffer is flushed, events will be discarded. --- @tparam[opt] int config.timeout The connection timeout to use when making --- requests to LaunchDarkly. --- @tparam[opt] int config.flushInterval he time between flushes of the event --- buffer. Decreasing the flush interval means that the event buffer is less --- likely to reach capacity. --- @tparam[opt] int config.pollInterval The polling interval --- (when streaming is disabled) in milliseconds. --- @tparam[opt] boolean config.offline Sets whether this client is offline. --- An offline client will not make any network connections to LaunchDarkly, --- and will return default values for all feature flags. --- @tparam[opt] boolean config.allAttributesPrivate Sets whether or not all user --- attributes (other than the key) should be hidden from LaunchDarkly. If this --- is true, all user attribute values will be private, not just the attributes --- specified in PrivateAttributeNames. --- @tparam[opt] boolean config.inlineUsersInEvents Set to true if you need to --- see the full user details in every analytics event. --- @tparam[opt] int config.userKeysCapacity The number of user keys that the --- event processor can remember at an one time, so that duplicate user details --- will not be sent in analytics. --- @tparam[opt] int config.userKeysFlushInterval The interval at which the event --- processor will reset its set of known user keys, in milliseconds. --- @tparam[opt] table config.privateAttributeNames Marks a set of user attribute --- names private. Any users sent to LaunchDarkly with this configuration active --- will have attributes with these names removed. --- @tparam[opt] backend config.featureStoreBackend Persistent feature store --- backend. --- @tparam int timeoutMilliseconds How long to wait for flags to --- download. If the timeout is reached a non fully initialized client will be --- returned. --- @return A fresh client. -local function clientInit(config, timeoutMilliseconds) - local interface = {} - - if timeoutMilliseconds <= 0 then - timeoutMilliseconds = 1 - end - - --- An opaque client object - -- @type Client - local client = ffi.gc(so.LDClientInit(makeConfig(config), timeoutMilliseconds), so.LDClientClose) - - --- Check if a client has been fully initialized. This may be useful if the - -- initialization timeout was reached. - -- @class function - -- @name isInitialized - -- @treturn boolean true if fully initialized - interface.isInitialized = function() - return so.LDClientIsInitialized(client) - end - - --- Generates an identify event for a user. - -- @class function - -- @name identify - -- @tparam user user An opaque user object from @{makeUser} - -- @treturn nil - interface.identify = function(user) - so.LDClientIdentify(client, user) - end - - --- Whether the LaunchDarkly client is in offline mode. - -- @class function - -- @name isOffline - -- @treturn boolean true if offline - interface.isOffline = function() - so.LDClientIsOffline(client) - end - - --- Immediately flushes queued events. - -- @class function - -- @name flush - -- @treturn nil - interface.flush = function() - so.LDClientFlush(client) - end - - --- Reports that a user has performed an event. Custom data, and a metric - -- can be attached to the event as JSON. - -- @class function - -- @name track - -- @tparam string key The name of the event - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam[opt] table data A value to be associated with an event - -- @tparam[optchain] number metric A value to be associated with an event - -- @treturn nil - interface.track = function(key, user, data, metric) - local json = nil - - if data ~= nil then - json = toLaunchDarklyJSON(data) - end - - if metric ~= nil then - so.LDClientTrackMetric(client, key, user, json, metric) - else - so.LDClientTrack(client, key, user, json) - end - end - - --- Returns a map from feature flag keys to values for a given user. - -- This does not send analytics events back to LaunchDarkly. - -- @class function - -- @name allFlags - -- @tparam user user An opaque user object from @{makeUser} - -- @treturn table - interface.allFlags = function(user) - local x = so.LDAllFlags(client, user) - if x ~= nil then - return fromLaunchDarklyJSON(x) - else - return nil - end - end - - --- Evaluate a boolean flag - -- @class function - -- @name boolVariation - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam boolean fallback The value to return on error - -- @treturn boolean The evaluation result, or the fallback value - interface.boolVariation = function(user, key, fallback) - return so.LDBoolVariation(client, user, key, fallback, nil) - end - - --- Evaluate a boolean flag and return an explanation - -- @class function - -- @name boolVariationDetail - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam boolean fallback The value to return on error - -- @treturn table The evaluation explanation - interface.boolVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, so.LDBoolVariation, nil) - end - - --- Evaluate an integer flag - -- @class function - -- @name intVariation - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam int fallback The value to return on error - -- @treturn int The evaluation result, or the fallback value - interface.intVariation = function(user, key, fallback) - return so.LDIntVariation(client, user, key, fallback, nil) - end - - --- Evaluate an integer flag and return an explanation - -- @class function - -- @name intVariationDetail - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam int fallback The value to return on error - -- @treturn table The evaluation explanation - interface.intVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, so.LDIntVariation, nil) - end - - --- Evaluate a double flag - -- @class function - -- @name doubleVariation - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam number fallback The value to return on error - -- @treturn double The evaluation result, or the fallback value - interface.doubleVariation = function(user, key, fallback) - return so.LDDoubleVariation(client, user, key, fallback, nil) - end - - --- Evaluate a double flag and return an explanation - -- @class function - -- @name doubleVariationDetail - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam number fallback The value to return on error - -- @treturn table The evaluation explanation - interface.doubleVariationDetail = function(user, key, fallback) - return genericVariationDetail(client, user, key, fallback, so.LDDoubleVariation, nil) - end - - --- Evaluate a string flag - -- @class function - -- @name stringVariation - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam string fallback The value to return on error - -- @treturn string The evaluation result, or the fallback value - interface.stringVariation = function(user, key, fallback) - local raw = so.LDStringVariation(client, user, key, fallback, nil) - local native = ffi.string(raw) - so.LDFree(raw) - return native - end - - --- Evaluate a string flag and return an explanation - -- @class function - -- @name stringVariationDetail - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam string fallback The value to return on error - -- @treturn table The evaluation explanation - interface.stringVariationDetail = function(user, key, fallback) - local valueConverter = function(raw) - local native = ffi.string(raw) - so.LDFree(raw) - return native - end - - return genericVariationDetail(client, user, key, fallback, so.LDStringVariation, valueConverter) - end - - --- Evaluate a json flag - -- @class function - -- @name jsonVariation - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam table fallback The value to return on error - -- @treturn table The evaluation result, or the fallback value - interface.jsonVariation = function(user, key, fallback) - local raw = so.LDJSONVariation(client, user, key, toLaunchDarklyJSON(fallback), nil) - local native = fromLaunchDarklyJSON(raw) - so.LDJSONFree(raw) - return native - end - - --- Evaluate a json flag and return an explanation - -- @class function - -- @name jsonVariationDetail - -- @tparam user user An opaque user object from @{makeUser} - -- @tparam string key The key of the flag to evaluate. - -- @tparam table fallback The value to return on error - -- @treturn table The evaluation explanation - interface.jsonVariationDetail = function(user, key, fallback) - local valueConverter = function(raw) - local native = fromLaunchDarklyJSON(raw) - so.LDJSONFree(raw) - return native - end - - return genericVariationDetail(client, user, key, toLaunchDarklyJSON(fallback), so.LDJSONVariation, valueConverter) - end - - --- @type end - - return interface -end - ---- @export -return { - registerLogger = registerLogger, - makeUser = makeUser, - clientInit = clientInit -} diff --git a/test.lua b/test.lua index d8c267d..f23793d 100644 --- a/test.lua +++ b/test.lua @@ -1,6 +1,6 @@ local u = require('luaunit') -local l = require("launchdarkly-server-sdk") -local r = require("launchdarkly-server-sdk-redis") +local l = require("launchdarkly_server_sdk") +local r = require("launchdarkly_server_sdk_redis") function logger(level, line) print(level .. ": " .. line) @@ -27,27 +27,86 @@ end function TestAll:testBoolVariation() local e = false - u.assertEquals(e, makeTestClient().boolVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():boolVariation(user, "test", e)) +end + +function TestAll:testBoolVariationDetail() + local e = { + value = true, + reason = { + kind = "ERROR", + errorKind = "CLIENT_NOT_READY" + } + } + u.assertEquals(e, makeTestClient():boolVariationDetail(user, "test", true)) end function TestAll:testIntVariation() local e = 3 - u.assertEquals(e, makeTestClient().intVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():intVariation(user, "test", e)) +end + +function TestAll:testIntVariationDetail() + local e = { + value = 5, + reason = { + kind = "ERROR", + errorKind = "CLIENT_NOT_READY" + } + } + u.assertEquals(e, makeTestClient():intVariationDetail(user, "test", 5)) end function TestAll:testDoubleVariation() local e = 5.3 - u.assertEquals(e, makeTestClient().doubleVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():doubleVariation(user, "test", e)) +end + +function TestAll:testDoubleVariationDetail() + local e = { + value = 6.2, + reason = { + kind = "ERROR", + errorKind = "CLIENT_NOT_READY" + } + } + u.assertEquals(e, makeTestClient():doubleVariationDetail(user, "test", 6.2)) end function TestAll:testStringVariation() local e = "a" - u.assertEquals(e, makeTestClient().stringVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():stringVariation(user, "test", e)) +end + +function TestAll:testStringVariationDetail() + local e = { + value = "f", + reason = { + kind = "ERROR", + errorKind = "CLIENT_NOT_READY" + } + } + u.assertEquals(e, makeTestClient():stringVariationDetail(user, "test", "f")) end function TestAll:testJSONVariation() local e = { ["a"] = "b" } - u.assertEquals(e, makeTestClient().jsonVariation(user, "test", e)) + u.assertEquals(e, makeTestClient():jsonVariation(user, "test", e)) +end + +function TestAll:testJSONVariationDetail() + local e = { + value = { a = "b" }, + reason = { + kind = "ERROR", + errorKind = "CLIENT_NOT_READY" + } + } + u.assertEquals(e, makeTestClient():jsonVariationDetail(user, "test", { a = "b" })) +end + +function TestAll:testIdentify() + makeTestClient():identify(user) end function TestAll:testRedisBasic() @@ -59,7 +118,7 @@ function TestAll:testRedisBasic() local e = false - u.assertEquals(e, makeTestClient().boolVariation(user, "test", e)) + u.assertEquals(e, c:boolVariation(user, "test", e)) end local runner = u.LuaUnit.new() From 8a766384d90bdf9b362526ba2fb5e81a47b06652 Mon Sep 17 00:00:00 2001 From: hroederld Date: Mon, 20 Jul 2020 13:40:52 -0700 Subject: [PATCH 23/25] [ch83425] prepare beta 3 (#23) --- .ldrelease/update-version.sh | 2 +- README.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.ldrelease/update-version.sh b/.ldrelease/update-version.sh index 64d347a..02920ed 100755 --- a/.ldrelease/update-version.sh +++ b/.ldrelease/update-version.sh @@ -2,4 +2,4 @@ set -e -sed -i "s/local SDKVersion =.*/local SDKVersion = \"${LD_RELEASE_VERSION}\"/" 'launchdarkly-server-sdk.lua' +sed -i "s/#define SDKVersion .*/#define SDKVersion \"${LD_RELEASE_VERSION}\"/" 'launchdarkly-server-sdk.c' diff --git a/README.md b/README.md index 633d131..ce81f5f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ LaunchDarkly Server-Side SDK for Lua =========================== +*This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* + LaunchDarkly overview ------------------------- [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! @@ -11,7 +13,7 @@ LaunchDarkly overview Supported Lua versions ----------- -This version of the LaunchDarkly SDK is compatible with the Lua 5.1 interpreter, and LuaJIT. Lua 5.3 is not supported due to FFI constraints. +This version of the LaunchDarkly SDK is compatible with the Lua 5.1-5.3 interpreter, and LuaJIT. Supported C server-side SDK versions ----------- From 368253a490954be9cf6dd083da78fd4b57e49016 Mon Sep 17 00:00:00 2001 From: hroederld Date: Mon, 27 Jul 2020 12:59:14 -0700 Subject: [PATCH 24/25] remove beta warning (#24) --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index ce81f5f..873d997 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ LaunchDarkly Server-Side SDK for Lua =========================== -*This version of the SDK is a **beta** version and should not be considered ready for production use while this message is visible.* - LaunchDarkly overview ------------------------- [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! From fd7e7e3aa52bde9331a9a814e3f4ea7f01fafc4b Mon Sep 17 00:00:00 2001 From: hroederld Date: Mon, 27 Jul 2020 13:12:28 -0700 Subject: [PATCH 25/25] set source in rockspec (#25) --- launchdarkly-server-sdk-1.0-0.rockspec | 2 +- launchdarkly-server-sdk-redis-1.0-0.rockspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk-1.0-0.rockspec b/launchdarkly-server-sdk-1.0-0.rockspec index e58ed20..b3cd2b6 100644 --- a/launchdarkly-server-sdk-1.0-0.rockspec +++ b/launchdarkly-server-sdk-1.0-0.rockspec @@ -3,7 +3,7 @@ package = "launchdarkly-server-sdk" version = "1.0-0" source = { - url = "." -- not online yet! + url = "git+https://github.com/launchdarkly/lua-server-sdk.git" } dependencies = { diff --git a/launchdarkly-server-sdk-redis-1.0-0.rockspec b/launchdarkly-server-sdk-redis-1.0-0.rockspec index 1751204..07ef6b2 100644 --- a/launchdarkly-server-sdk-redis-1.0-0.rockspec +++ b/launchdarkly-server-sdk-redis-1.0-0.rockspec @@ -3,7 +3,7 @@ package = "launchdarkly-server-sdk-redis" version = "1.0-0" source = { - url = "." -- not online yet! + url = "git+https://github.com/launchdarkly/lua-server-sdk.git" } dependencies = {