diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..e765b52 --- /dev/null +++ b/.clang-format @@ -0,0 +1,6 @@ +BasedOnStyle: Chromium +IndentWidth: 4 +ColumnLimit: 160 +NamespaceIndentation: None +# The number of spaces before trailing line comments (// - comments). +SpacesBeforeTrailingComments: 1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e9730b..da3d826 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,7 +57,7 @@ if(CONFIGCAT_BUILD_TESTS) ) target_include_directories(google_tests PRIVATE ${CONFIGCAT_INCLUDE_PATHS}) # $ explicitly propagates private dependencies - target_link_libraries(google_tests configcat gtest_main $) + target_link_libraries(google_tests configcat gmock_main $) gtest_discover_tests(google_tests) endif() diff --git a/README.md b/README.md index 202c81c..353e87a 100644 --- a/README.md +++ b/README.md @@ -93,9 +93,9 @@ Read more about Targeting [here](https://configcat.com/docs/advanced/targeting/) Percentage and targeted rollouts are calculated by the user object passed to the configuration requests. The user object must be created with a **mandatory** identifier parameter which uniquely identifies each user: ```cpp -auto user = ConfigCatUser("#USER-IDENTIFIER#"); +auto user = ConfigCatUser::create("#USER-IDENTIFIER#"); -bool isMyAwesomeFeatureEnabled = client->getValue("isMyAwesomeFeatureEnabled", false, &user); +bool isMyAwesomeFeatureEnabled = client->getValue("isMyAwesomeFeatureEnabled", false, user); if (isMyAwesomeFeatureEnabled) { doTheNewThing(); } else { diff --git a/include/configcat/config.h b/include/configcat/config.h index 29a197c..5a29baf 100644 --- a/include/configcat/config.h +++ b/include/configcat/config.h @@ -1,211 +1,401 @@ #pragma once +#include +#include +#include +#include #include -#include #include +#include #include -#include -#include -#include namespace configcat { -using ValueType = std::variant; -// Disable implicit conversion from pointer types (const char*) to bool when constructing std::variant -// https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0608r3.html -struct Value : public ValueType { - Value() : ValueType() {} - Value(bool v) : ValueType(v) {} - Value(const char* v) : ValueType(std::string(v)) {} - Value(const std::string& v) : ValueType(v) {} - Value(int v) : ValueType(v) {} - Value(double v) : ValueType(v) {} +struct SettingValue; + +struct Value : public std::variant { +private: + using _Base = std::variant; +public: + Value(const char* v) : _Base(std::string(v)) {} + // Disable the implicit conversion from pointer to bool: https://stackoverflow.com/a/59372958/8656352 template Value(T*) = delete; + + using _Base::_Base; + using _Base::operator=; + + operator SettingValue() const; + + std::string toString() const; +}; + +inline std::ostream& operator<<(std::ostream& os, const Value& v) { + return os << v.toString(); +} + +inline std::ostream& operator<<(std::ostream& os, const std::optional& v) { + return os << (v ? v->toString() : ""); +} + +enum class RedirectMode : int { + No = 0, + Should = 1, + Force = 2 }; -std::string valueToString(const ValueType& value); +/** Setting type. */ +enum class SettingType : int { + /** On/off type (feature flag). */ + Boolean = 0, + /** Text type. */ + String = 1, + /** Whole number type. */ + Int = 2, + /** Decimal number type. */ + Double = 3, +}; -enum RedirectMode: int { - NoRedirect = 0, - ShouldRedirect = 1, - ForceRedirect = 2 +/** User Object attribute comparison operator used during the evaluation process. */ +enum class UserComparator { + /** IS ONE OF (cleartext) - Checks whether the comparison attribute is equal to any of the comparison values. */ + TextIsOneOf = 0, + /** IS NOT ONE OF (cleartext) - Checks whether the comparison attribute is not equal to any of the comparison values. */ + TextIsNotOneOf = 1, + /** CONTAINS ANY OF (cleartext) - Checks whether the comparison attribute contains any comparison values as a substring. */ + TextContainsAnyOf = 2, + /** NOT CONTAINS ANY OF (cleartext) - Checks whether the comparison attribute does not contain any comparison values as a substring. */ + TextNotContainsAnyOf = 3, + /** IS ONE OF (semver) - Checks whether the comparison attribute interpreted as a semantic version is equal to any of the comparison values. */ + SemVerIsOneOf = 4, + /** IS NOT ONE OF (semver) - Checks whether the comparison attribute interpreted as a semantic version is not equal to any of the comparison values. */ + SemVerIsNotOneOf = 5, + /** < (semver) - Checks whether the comparison attribute interpreted as a semantic version is less than the comparison value. */ + SemVerLess = 6, + /** <= (semver) - Checks whether the comparison attribute interpreted as a semantic version is less than or equal to the comparison value. */ + SemVerLessOrEquals = 7, + /** > (semver) - Checks whether the comparison attribute interpreted as a semantic version is greater than the comparison value. */ + SemVerGreater = 8, + /** >= (semver) - Checks whether the comparison attribute interpreted as a semantic version is greater than or equal to the comparison value. */ + SemVerGreaterOrEquals = 9, + /** = (number) - Checks whether the comparison attribute interpreted as a decimal number is equal to the comparison value. */ + NumberEquals = 10, + /** != (number) - Checks whether the comparison attribute interpreted as a decimal number is not equal to the comparison value. */ + NumberNotEquals = 11, + /** < (number) - Checks whether the comparison attribute interpreted as a decimal number is less than the comparison value. */ + NumberLess = 12, + /** <= (number) - Checks whether the comparison attribute interpreted as a decimal number is less than or equal to the comparison value. */ + NumberLessOrEquals = 13, + /** > (number) - Checks whether the comparison attribute interpreted as a decimal number is greater than the comparison value. */ + NumberGreater = 14, + /** >= (number) - Checks whether the comparison attribute interpreted as a decimal number is greater than or equal to the comparison value. */ + NumberGreaterOrEquals = 15, + /** IS ONE OF (hashed) - Checks whether the comparison attribute is equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveTextIsOneOf = 16, + /** IS NOT ONE OF (hashed) - Checks whether the comparison attribute is not equal to any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveTextIsNotOneOf = 17, + /** BEFORE (UTC datetime) - Checks whether the comparison attribute interpreted as the seconds elapsed since Unix Epoch is less than the comparison value. */ + DateTimeBefore = 18, + /** AFTER (UTC datetime) - Checks whether the comparison attribute interpreted as the seconds elapsed since Unix Epoch is greater than the comparison value. */ + DateTimeAfter = 19, + /** EQUALS (hashed) - Checks whether the comparison attribute is equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveTextEquals = 20, + /** NOT EQUALS (hashed) - Checks whether the comparison attribute is not equal to the comparison value (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveTextNotEquals = 21, + /** STARTS WITH ANY OF (hashed) - Checks whether the comparison attribute starts with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveTextStartsWithAnyOf = 22, + /** NOT STARTS WITH ANY OF (hashed) - Checks whether the comparison attribute does not start with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveTextNotStartsWithAnyOf = 23, + /** ENDS WITH ANY OF (hashed) - Checks whether the comparison attribute ends with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveTextEndsWithAnyOf = 24, + /** NOT ENDS WITH ANY OF (hashed) - Checks whether the comparison attribute does not end with any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveTextNotEndsWithAnyOf = 25, + /** ARRAY CONTAINS ANY OF (hashed) - Checks whether the comparison attribute interpreted as a comma-separated list contains any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveArrayContainsAnyOf = 26, + /** ARRAY NOT CONTAINS ANY OF (hashed) - Checks whether the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values (where the comparison is performed using the salted SHA256 hashes of the values). */ + SensitiveArrayNotContainsAnyOf = 27, + /** EQUALS (cleartext) - Checks whether the comparison attribute is equal to the comparison value. */ + TextEquals = 28, + /** NOT EQUALS (cleartext) - Checks whether the comparison attribute is not equal to the comparison value. */ + TextNotEquals = 29, + /** STARTS WITH ANY OF (cleartext) - Checks whether the comparison attribute starts with any of the comparison values. */ + TextStartsWithAnyOf = 30, + /** NOT STARTS WITH ANY OF (cleartext) - Checks whether the comparison attribute does not start with any of the comparison values. */ + TextNotStartsWithAnyOf = 31, + /** ENDS WITH ANY OF (cleartext) - Checks whether the comparison attribute ends with any of the comparison values. */ + TextEndsWithAnyOf = 32, + /** NOT ENDS WITH ANY OF (cleartext) - Checks whether the comparison attribute does not end with any of the comparison values. */ + TextNotEndsWithAnyOf = 33, + /** ARRAY CONTAINS ANY OF (cleartext) - Checks whether the comparison attribute interpreted as a comma-separated list contains any of the comparison values. */ + ArrayContainsAnyOf = 34, + /** ARRAY NOT CONTAINS ANY OF (cleartext) - Checks whether the comparison attribute interpreted as a comma-separated list does not contain any of the comparison values. */ + ArrayNotContainsAnyOf = 35, }; -struct Preferences { - std::string url; - RedirectMode redirect = NoRedirect; +/** Prerequisite flag comparison operator used during the evaluation process. */ +enum class PrerequisiteFlagComparator { + /** EQUALS - Checks whether the evaluated value of the specified prerequisite flag is equal to the comparison value. */ + Equals = 0, + /** NOT EQUALS - Checks whether the evaluated value of the specified prerequisite flag is not equal to the comparison value. */ + NotEquals = 1 }; -struct RolloutPercentageItem { - // Value served when the rule is selected during evaluation. - Value value = 0; +/** Segment comparison operator used during the evaluation process. */ +enum class SegmentComparator { + /** IS IN SEGMENT - Checks whether the conditions of the specified segment are evaluated to true. */ + IsIn = 0, + /** IS NOT IN SEGMENT - Checks whether the conditions of the specified segment are evaluated to false. */ + IsNotIn = 1, +}; - // The rule's percentage value. - double percentage = 0.0; +template +class one_of : public std::variant { + using _Base = std::variant; +public: + one_of() : _Base(std::nullopt) {} + using _Base::_Base; // inherit std::variant's ctors + using _Base::operator=; // inherit std::variant's assignment operators +}; - // The rule's variation ID (for analytical purposes). - std::string variationId; +std::ostream& operator<<(std::ostream& os, const Value& v); - bool operator==(const RolloutPercentageItem& other) const { - return value == other.value - && percentage == other.percentage - && variationId == other.variationId; +struct SettingValuePrivate; + +struct SettingValue : public one_of { + // Not for maintainers: the indices of variant alternatives must correspond to + // the SettingType enum's member values because we rely on this! (See also getSettingType) +private: + using _Base = one_of; +protected: + friend struct SettingValuePrivate; + + struct UnsupportedValue { + std::string type; + std::string value; + }; + + std::shared_ptr unsupportedValue; +public: + static constexpr char kBoolean[] = "b"; + static constexpr char kString[] = "s"; + static constexpr char kInt[] = "i"; + static constexpr char kDouble[] = "d"; + + SettingValue(const char* v) : _Base(std::string(v)) {} + + using _Base::_Base; + using _Base::operator=; + + operator std::optional() const; + std::optional toValueChecked(SettingType type, bool throwIfInvalid = true) const; + + inline SettingType getSettingType() const { + // For unsupported values this results in -1, which value is not present in the SettingType enum. + // However, we only use this value internally (will never expose it to the end user). + return static_cast(this->index() - 1); } }; -enum Comparator: int { - ONE_OF = 0, // IS ONE OF - NOT_ONE_OF = 1, // IS NOT ONE OF - CONTAINS = 2, // CONTAINS - NOT_CONTAINS = 3, // DOES NOT CONTAIN - ONE_OF_SEMVER = 4, // IS ONE OF (SemVer) - NOT_ONE_OF_SEMVER = 5, // IS NOT ONE OF (SemVer) - LT_SEMVER = 6, // < (SemVer) - LTE_SEMVER = 7, // <= (SemVer) - GT_SEMVER = 8, // > (SemVer) - GTE_SEMVER = 9, // >= (SemVer) - EQ_NUM = 10, // = (Number) - NOT_EQ_NUM = 11, // <> (Number) - LT_NUM = 12, // < (Number) - LTE_NUM = 13, // <= (Number) - GT_NUM = 14, // > (Number) - GTE_NUM = 15, // >= (Number) - ONE_OF_SENS = 16, // IS ONE OF (Sensitive) - NOT_ONE_OF_SENS = 17 // IS NOT ONE OF (Sensitive) -}; - -static constexpr char const* kComparatorTexts[] = { - "IS ONE OF", - "IS NOT ONE OF", - "CONTAINS", - "DOES NOT CONTAIN", - "IS ONE OF (SemVer)", - "IS NOT ONE OF (SemVer)", - "< (SemVer)", - "<= (SemVer)", - "> (SemVer)", - ">= (SemVer)", - "= (Number)", - "<> (Number)", - "< (Number)", - "<= (Number)", - "> (Number)", - ">= (Number)", - "IS ONE OF (Sensitive)", - "IS NOT ONE OF (Sensitive)" -}; - -inline const char* comparatorToString(Comparator comparator) { - return kComparatorTexts[comparator]; -} +struct SettingValueContainer { + static constexpr char kValue[] = "v"; + static constexpr char kVariationId[] = "i"; + + SettingValue value; + std::optional variationId; +}; + +struct PercentageOption : public SettingValueContainer { + static constexpr char kPercentage[] = "p"; + + uint8_t percentage = 0; +}; + +using PercentageOptions = std::vector; -struct RolloutRule { - // Value served when the rule is selected during evaluation. - Value value = 0; // optional +using UserConditionComparisonValue = one_of>; + +struct UserCondition { + static constexpr char kComparisonAttribute[] = "a"; + static constexpr char kComparator[] = "c"; + static constexpr char kStringComparisonValue[] = "s"; + static constexpr char kNumberComparisonValue[] = "d"; + static constexpr char kStringListComparisonValue[] = "l"; - // The user attribute used in the comparison during evaluation. std::string comparisonAttribute; + UserComparator comparator = static_cast(-1); + UserConditionComparisonValue comparisonValue; +}; - // The operator used in the comparison. - Comparator comparator = ONE_OF; +using UserConditions = std::vector; - // The comparison value compared to the given user attribute. - std::string comparisonValue; +struct PrerequisiteFlagCondition { + static constexpr char kPrerequisiteFlagKey[] = "f"; + static constexpr char kComparator[] = "c"; + static constexpr char kComparisonValue[] = "v"; - // The rule's variation ID (for analytical purposes). - std::string variationId; + std::string prerequisiteFlagKey; + PrerequisiteFlagComparator comparator = static_cast(-1); + SettingValue comparisonValue; +}; - bool operator==(const RolloutRule& other) const { - return value == other.value && - comparisonAttribute == other.comparisonAttribute && - comparator == other.comparator && - comparisonValue == other.comparisonValue && - variationId == other.variationId; - } +struct SegmentCondition { + static constexpr char kSegmentIndex[] = "s"; + static constexpr char kComparator[] = "c"; + + int32_t segmentIndex = -1; + SegmentComparator comparator = static_cast(-1); }; -struct Setting { - // Value of the feature flag / setting. - Value value = 0; +using Condition = one_of; + +struct ConditionContainer { + static constexpr char kUserCondition[] = "u"; + static constexpr char kPrerequisiteFlagCondition[] = "p"; + static constexpr char kSegmentCondition[] = "s"; - // Collection of percentage rules that belongs to the feature flag / setting. - std::vector percentageItems; + Condition condition; +}; - // Collection of targeting rules that belongs to the feature flag / setting. - std::vector rolloutRules; +using Conditions = std::vector; - // Variation ID (for analytical purposes). - std::string variationId; +using TargetingRuleThenPart = one_of; - bool operator==(const Setting& other) const { - return value == other.value - && percentageItems == other.percentageItems - && rolloutRules == other.rolloutRules - && variationId == other.variationId; - } +struct TargetingRule { + static constexpr char kConditions[] = "c"; + static constexpr char kSimpleValue[] = "s"; + static constexpr char kPercentageOptions[] = "p"; + + Conditions conditions; + TargetingRuleThenPart then; +}; + +using TargetingRules = std::vector; + +struct Segment { + static constexpr char kName[] = "n"; + static constexpr char kConditions[] = "r"; + + std::string name; + UserConditions conditions; +}; + +using Segments = std::vector; + +struct Config; +class RolloutEvaluator; + +struct Setting : public SettingValueContainer { + static constexpr char kType[] = "t"; + static constexpr char kPercentageOptionsAttribute[] = "a"; + static constexpr char kTargetingRules[] = "r"; + static constexpr char kPercentageOptions[] = "p"; + + static Setting fromValue(const SettingValue& value); + + SettingType type = static_cast(-1); + std::optional percentageOptionsAttribute; + TargetingRules targetingRules; + PercentageOptions percentageOptions; + + inline bool hasInvalidType() const { return type < SettingType::Boolean || SettingType::Double < type; } + SettingType getTypeChecked() const; + +protected: + friend struct Config; + friend class RolloutEvaluator; + std::shared_ptr configJsonSalt; + std::shared_ptr segments; }; using Settings = std::unordered_map; +struct Preferences { + /** + * The JSON key of the base url from where the config.json is intended to be downloaded. + */ + static constexpr char kBaseUrl[] = "u"; + /** + * The JSON key of the redirect mode that should be used in case the data governance mode is wrongly configured. + */ + static constexpr char kRedirectMode[] = "r"; + /** + * The JSON key of The salt that, combined with the feature flag key or segment name, is used to hash values for sensitive text comparisons. + */ + static constexpr char kSalt[] = "s"; + + std::optional baseUrl; + RedirectMode redirectMode = RedirectMode::No; + std::shared_ptr salt; + + Preferences() {} + + Preferences(const Preferences& other) : Preferences() { *this = other; } + + Preferences& operator=(const Preferences& other) { + baseUrl = other.baseUrl; + redirectMode = other.redirectMode; + salt = other.salt ? std::make_shared(*other.salt) : nullptr; + return *this; + } + + Preferences(Preferences&& other) noexcept = default; + + Preferences& operator=(Preferences&& other) noexcept = default; +}; + +/** + * Details of a ConfigCat config. + */ struct Config { - static constexpr char kValue[] = "v"; - static constexpr char kComparator[] = "t"; - static constexpr char kComparisonAttribute[] = "a"; - static constexpr char kComparisonValue[] = "c"; - static constexpr char kRolloutPercentageItems[] = "p"; - static constexpr char kPercentage[] = "p"; - static constexpr char kRolloutRules[] = "r"; - static constexpr char kVariationId[] = "i"; + /** + * The JSON key of preferences of the config.json, mostly for controlling the redirection behaviour of the SDK. + */ static constexpr char kPreferences[] = "p"; - static constexpr char kPreferencesUrl[] = "u"; - static constexpr char kPreferencesRedirect[] = "r"; - static constexpr char kEntries[] = "f"; + /** + * The JSON key of segment definitions for re-using segment rules in targeting rules. + */ + static constexpr char kSegments[] = "s"; + /** + * The JSON key of setting definitions. + */ + static constexpr char kSettings[] = "f"; - std::shared_ptr preferences; - std::shared_ptr entries; + static const std::shared_ptr empty; std::string toJson(); - static std::shared_ptr fromJson(const std::string& jsonString); - static std::shared_ptr fromFile(const std::string& filePath); - - static std::shared_ptr empty; + static std::shared_ptr fromJson(const std::string& jsonString, bool tolerant = false); + static std::shared_ptr fromFile(const std::string& filePath, bool tolerant = true); - Config() {}; - Config(const Config&) = delete; // Disable copy -}; + std::optional preferences; + std::shared_ptr segments; + std::shared_ptr settings; -// extra brackets to avoid numeric_limits::max()/min() not recognized error on windows -constexpr double kDistantFuture = (std::numeric_limits::max)(); -constexpr double kDistantPast = (std::numeric_limits::min)(); + std::shared_ptr getSegmentsOrEmpty() const { return segments ? segments : std::make_shared(); } + std::shared_ptr getSettingsOrEmpty() const { return settings ? settings : std::make_shared(); } -struct ConfigEntry { - static constexpr char kConfig[] = "config"; - static constexpr char kETag[] = "etag"; - static constexpr char kFetchTime[] = "fetch_time"; - static constexpr char kSerializationFormatVersion[] = "v2"; + Config() {} - static std::shared_ptr empty; + Config(const Config& other) + : preferences(other.preferences) + , segments(other.segments ? std::make_shared(*other.segments) : nullptr) + , settings(other.settings ? std::make_shared(*other.settings) : nullptr) { + fixupSaltAndSegments(); + } - ConfigEntry(std::shared_ptr config = Config::empty, - const std::string& eTag = "", - const std::string& configJsonString = "{}", - double fetchTime = kDistantPast): - config(config), - eTag(eTag), - configJsonString(configJsonString), - fetchTime(fetchTime) { + Config& operator=(const Config& other) { + preferences = other.preferences; + segments = other.segments ? std::make_shared(*other.segments) : nullptr; + settings = other.settings ? std::make_shared(*other.settings) : nullptr; + fixupSaltAndSegments(); + return *this; } - ConfigEntry(const ConfigEntry&) = delete; // Disable copy - static std::shared_ptr fromString(const std::string& text); - std::string serialize() const; + Config(Config&& other) noexcept = default; - std::shared_ptr config; - std::string eTag; - std::string configJsonString; - double fetchTime; + Config& operator=(Config&& other) noexcept = default; +private: + void fixupSaltAndSegments(); }; } // namespace configcat diff --git a/include/configcat/configcatclient.h b/include/configcat/configcatclient.h index 5591528..ffbf8ad 100644 --- a/include/configcat/configcatclient.h +++ b/include/configcat/configcatclient.h @@ -8,7 +8,6 @@ #include "keyvalue.h" #include "configcatoptions.h" #include "refreshresult.h" -#include "settingresult.h" #include "evaluationdetails.h" @@ -20,15 +19,21 @@ class ConfigFetcher; class RolloutEvaluator; class FlagOverrides; class ConfigService; - +struct SettingResult; class ConfigCatClient { public: + ConfigCatClient(const ConfigCatClient&) = delete; // Disable copy + ConfigCatClient& operator=(const ConfigCatClient&) = delete; + + ConfigCatClient(ConfigCatClient&&) = delete; // Disable move + ConfigCatClient& operator=(ConfigCatClient&&) = delete; + // Creates a new or gets an already existing [ConfigCatClient] for the given [sdkKey]. - static ConfigCatClient* get(const std::string& sdkKey, const ConfigCatOptions* options = nullptr); + static std::shared_ptr get(const std::string& sdkKey, const ConfigCatOptions* options = nullptr); // Closes an individual [ConfigCatClient] instance. - static void close(ConfigCatClient* client); + static void close(const std::shared_ptr& client); // Closes all [ConfigCatClient] instances. static void closeAll(); @@ -43,17 +48,17 @@ class ConfigCatClient { * Parameter [defaultValue]: in case of any failure, this value will be returned. * Parameter [user]: the user object to identify the caller. */ - bool getValue(const std::string& key, bool defaultValue, const ConfigCatUser* user = nullptr) const; - int getValue(const std::string& key, int defaultValue, const ConfigCatUser* user = nullptr) const; - double getValue(const std::string& key, double defaultValue, const ConfigCatUser* user = nullptr) const; - std::string getValue(const std::string& key, const char* defaultValue, const ConfigCatUser* user = nullptr) const; - std::string getValue(const std::string& key, const std::string& defaultValue, const ConfigCatUser* user = nullptr) const; + bool getValue(const std::string& key, bool defaultValue, const std::shared_ptr& user = nullptr) const; + int32_t getValue(const std::string& key, int32_t defaultValue, const std::shared_ptr& user = nullptr) const; + double getValue(const std::string& key, double defaultValue, const std::shared_ptr& user = nullptr) const; + std::string getValue(const std::string& key, const char* defaultValue, const std::shared_ptr& user = nullptr) const; + std::string getValue(const std::string& key, const std::string& defaultValue, const std::shared_ptr& user = nullptr) const; /** - * Gets the value of a feature flag or setting as std::shared_ptr identified by the given [key]. - * In case of any failure, nullptr will be returned. The [user] param identifies the caller. + * Gets the value of a feature flag or setting as std::optional identified by the given [key]. + * In case of any failure, std::nullopt will be returned. The [user] param identifies the caller. */ - std::shared_ptr getValue(const std::string& key, const ConfigCatUser* user = nullptr) const; + std::optional getValue(const std::string& key, const std::shared_ptr& user = nullptr) const; /** * Gets the value and evaluation details of a feature flag or setting identified by the given `key`. @@ -62,34 +67,40 @@ class ConfigCatClient { * Parameter [defaultValue]: in case of any failure, this value will be returned. * Parameter [user]: the user object to identify the caller. */ - EvaluationDetails getValueDetails(const std::string& key, bool defaultValue, const ConfigCatUser* user = nullptr) const; - EvaluationDetails getValueDetails(const std::string& key, int defaultValue, const ConfigCatUser* user = nullptr) const; - EvaluationDetails getValueDetails(const std::string& key, double defaultValue, const ConfigCatUser* user = nullptr) const; - EvaluationDetails getValueDetails(const std::string& key, const std::string& defaultValue, const ConfigCatUser* user = nullptr) const; - EvaluationDetails getValueDetails(const std::string& key, const char* defaultValue, const ConfigCatUser* user = nullptr) const; + EvaluationDetails getValueDetails(const std::string& key, bool defaultValue, const std::shared_ptr& user = nullptr) const; + EvaluationDetails getValueDetails(const std::string& key, int32_t defaultValue, const std::shared_ptr& user = nullptr) const; + EvaluationDetails getValueDetails(const std::string& key, double defaultValue, const std::shared_ptr& user = nullptr) const; + EvaluationDetails getValueDetails(const std::string& key, const std::string& defaultValue, const std::shared_ptr& user = nullptr) const; + EvaluationDetails getValueDetails(const std::string& key, const char* defaultValue, const std::shared_ptr& user = nullptr) const; + + /** + * Gets the value and evaluation details of a feature flag or setting identified by the given [key]. + * In case of any failure, the [value] field of the returned EvaluationDetails struct will be set to std::nullopt. The [user] param identifies the caller. + */ + EvaluationDetails> getValueDetails(const std::string& key, const std::shared_ptr& user = nullptr) const; // Gets all the setting keys. std::vector getAllKeys() const; // Gets the key of a setting and it's value identified by the given Variation ID (analytics) - std::shared_ptr getKeyAndValue(const std::string& variationId) const; + std::optional getKeyAndValue(const std::string& variationId) const; // Gets the values of all feature flags or settings. - std::unordered_map getAllValues(const ConfigCatUser* user = nullptr) const; + std::unordered_map getAllValues(const std::shared_ptr& user = nullptr) const; // Gets the values along with evaluation details of all feature flags and settings. - std::vector getAllValueDetails(const ConfigCatUser* user = nullptr) const; + std::vector> getAllValueDetails(const std::shared_ptr& user = nullptr) const; // Initiates a force refresh synchronously on the cached configuration. - void forceRefresh(); + RefreshResult forceRefresh(); // Sets the default user. - void setDefaultUser(std::shared_ptr user) { + inline void setDefaultUser(const std::shared_ptr& user) { defaultUser = user; } // Sets the default user to nullptr. - void clearDefaultUser() { + inline void clearDefaultUser() { defaultUser.reset(); } @@ -103,23 +114,30 @@ class ConfigCatClient { bool isOffline() const; // Gets the Hooks object for subscribing events. - std::shared_ptr getHooks() { return hooks; } + inline std::shared_ptr getHooks() { return hooks; } private: + struct MakeSharedEnabler; + ConfigCatClient(const std::string& sdkKey, const ConfigCatOptions& options); + void closeResources(); + template - ValueType _getValue(const std::string& key, const ValueType& defaultValue, const ConfigCatUser* user = nullptr) const; + ValueType _getValue(const std::string& key, const ValueType& defaultValue, const std::shared_ptr& user = nullptr) const; template - EvaluationDetails _getValueDetails(const std::string& key, ValueType defaultValue, const ConfigCatUser* user = nullptr) const; + EvaluationDetails _getValueDetails(const std::string& key, const ValueType& defaultValue, const std::shared_ptr& user = nullptr) const; SettingResult getSettings() const; - EvaluationDetails evaluate(const std::string& key, - const ConfigCatUser* user, - const Setting& setting, - double fetchTime) const; + template + EvaluationDetails evaluate(const std::string& key, + const std::optional& defaultValue, + const std::shared_ptr& effectiveUser, + const Setting& setting, + const std::shared_ptr& settings, + double fetchTime) const; std::shared_ptr hooks; std::shared_ptr logger; @@ -129,7 +147,7 @@ class ConfigCatClient { std::unique_ptr configService; static std::mutex instancesMutex; - static std::unordered_map> instances; + static std::unordered_map> instances; }; } // namespace configcat diff --git a/include/configcat/configcatoptions.h b/include/configcat/configcatoptions.h index 90a412d..9a8e2fd 100644 --- a/include/configcat/configcatoptions.h +++ b/include/configcat/configcatoptions.h @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "datagovernance.h" #include "pollingmode.h" #include "configcache.h" @@ -21,9 +23,9 @@ namespace configcat { class Hooks { public: explicit Hooks(const std::function& onClientReady = nullptr, - const std::function)>& onConfigChanged = nullptr, - const std::function& onFlagEvaluated = nullptr, - const std::function& onError = nullptr) { + const std::function)>& onConfigChanged = nullptr, + const std::function& onFlagEvaluated = nullptr, + const std::function& onError = nullptr) { if (onClientReady) { onClientReadyCallbacks.push_back(onClientReady); } @@ -43,17 +45,17 @@ class Hooks { onClientReadyCallbacks.push_back(callback); } - void addOnConfigChanged(const std::function)>& callback) { + void addOnConfigChanged(const std::function)>& callback) { std::lock_guard lock(mutex); onConfigChangedCallbacks.push_back(callback); } - void addOnFlagEvaluated(const std::function& callback) { + void addOnFlagEvaluated(const std::function& callback) { std::lock_guard lock(mutex); onFlagEvaluatedCallbacks.push_back(callback); } - void addOnError(const std::function& callback) { + void addOnError(const std::function& callback) { std::lock_guard lock(mutex); onErrorCallbacks.push_back(callback); } @@ -65,24 +67,24 @@ class Hooks { } } - void invokeOnConfigChanged(std::shared_ptr config) { + void invokeOnConfigChanged(const std::shared_ptr& config) { std::lock_guard lock(mutex); for (auto& callback : onConfigChangedCallbacks) { callback(config); } } - void invokeOnFlagEvaluated(const EvaluationDetails& details) { + void invokeOnFlagEvaluated(const EvaluationDetailsBase& details) { std::lock_guard lock(mutex); for (auto& callback : onFlagEvaluatedCallbacks) { callback(details); } } - void invokeOnError(const std::string& error) { + void invokeOnError(const std::string& message, const std::exception_ptr& exception) { std::lock_guard lock(mutex); for (auto& callback : onErrorCallbacks) { - callback(error); + callback(message, exception); } } @@ -97,9 +99,9 @@ class Hooks { private: std::mutex mutex; std::vector> onClientReadyCallbacks; - std::vector)>> onConfigChangedCallbacks; - std::vector> onFlagEvaluatedCallbacks; - std::vector> onErrorCallbacks; + std::vector)>> onConfigChangedCallbacks; + std::vector> onFlagEvaluatedCallbacks; + std::vector> onErrorCallbacks; }; // Configuration options for ConfigCatClient. diff --git a/include/configcat/configcatuser.h b/include/configcat/configcatuser.h index 6da6b0b..92f15a1 100644 --- a/include/configcat/configcatuser.h +++ b/include/configcat/configcatuser.h @@ -1,26 +1,101 @@ #pragma once +#include +#include +#include #include #include +#include +#include +#include "timeutils.h" namespace configcat { // An object containing attributes to properly identify a given user for rollout evaluation. class ConfigCatUser { public: + struct AttributeValue : public std::variant> { + private: + using _Base = std::variant>; + public: + AttributeValue(const char* v) : _Base(std::string(v)) {} + // CLang number type conversion to variant fix + AttributeValue(double value) : _Base(value) {} + + // Disable the implicit conversion from pointer to bool: https://stackoverflow.com/a/59372958/8656352 + template + AttributeValue(T*) = delete; + + using _Base::_Base; + using _Base::operator=; + }; + + static constexpr char kIdentifierAttribute[] = "Identifier"; + static constexpr char kEmailAttribute[] = "Email"; + static constexpr char kCountryAttribute[] = "Country"; + + /** + * Creates a new instance of the [ConfigCatUser] class. + * + * Parameter [id]: the unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) + * Parameter [email]: email address of the user. + * Parameter [country]: country of the user. + * Parameter [custom]: custom attributes of the user for advanced targeting rule definitions (e.g. user role, subscription type, etc.) + * + * All comparators support `std::string` values as User Object attribute (in some cases they need to be provided in a specific format though, see below), + * but some of them also support other types of values. It depends on the comparator how the values will be handled. The following rules apply: + * + * **Text-based comparators** (EQUALS, IS ONE OF, etc.) + * * accept `std::string` values, + * * all other values are automatically converted to `std::string` (a warning will be logged but evaluation will continue as normal). + * + * **SemVer-based comparators** (IS ONE OF, <, >=, etc.) + * * accept `std::string` values containing a properly formatted, valid semver value, + * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + * + * **Number-based comparators** (=, <, >=, etc.) + * * accept `double` values, + * * accept `std::string` values containing a properly formatted, valid `double` value, + * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + * + * **Date time-based comparators** (BEFORE / AFTER) + * * accept `configcat::date_time_t` (`std::chrono::system_clock::time_point`) values, + which are automatically converted to a second-based Unix timestamp, + * * accept `double` values representing a second-based Unix timestamp, + * * accept `std::string` values containing a properly formatted, valid `double` value, + * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + * + * **String array-based comparators** (ARRAY CONTAINS ANY OF / ARRAY NOT CONTAINS ANY OF) + * * accept lists of `std::string` (i.e. `std::vector`), + * * accept `std::string` values containing a valid JSON string which can be deserialized to a list of `std::string`, + * * all other values are considered invalid (a warning will be logged and the currently evaluated targeting rule will be skipped). + */ ConfigCatUser(const std::string& id, - const std::string& email = {}, - const std::string& country = {}, - const std::unordered_map& custom = {}); + const std::optional& email = std::nullopt, + const std::optional& country = std::nullopt, + const std::unordered_map& custom = {}) + : identifier(id) + , email(email) + , country(country) + , custom(custom) {} - const std::string* getAttribute(const std::string& key) const; + static inline std::shared_ptr create(const std::string& id, + const std::optional& email = std::nullopt, + const std::optional& country = std::nullopt, + const std::unordered_map& custom = {}) { + return std::make_shared(id, email, country, custom); + } + + inline const std::string& getIdentifier() const { return std::get(identifier); } + inline const ConfigCatUser::AttributeValue& getIdentifierAttribute() const { return identifier; } + const ConfigCatUser::AttributeValue* getAttribute(const std::string& key) const; std::string toJson() const; private: - std::unordered_map attributes; - -public: - const std::string& identifier; + ConfigCatUser::AttributeValue identifier; + std::optional email; + std::optional country; + std::unordered_map custom; }; } // namespace configcat diff --git a/include/configcat/consolelogger.h b/include/configcat/consolelogger.h index c779b90..da0f2a6 100644 --- a/include/configcat/consolelogger.h +++ b/include/configcat/consolelogger.h @@ -9,8 +9,9 @@ class ConsoleLogger : public ILogger { public: ConsoleLogger(LogLevel logLevel = LOG_LEVEL_WARNING): ILogger(logLevel) {} - void log(LogLevel level, const std::string& message) override { + void log(LogLevel level, const std::string& message, const std::exception_ptr& exception = nullptr) override { printf("[%s]: %s\n", logLevelAsString(level), message.c_str()); + if (exception) printf("Exception details: %s\n", unwrap_exception_message(exception).c_str()); } }; diff --git a/include/configcat/evaluationdetails.h b/include/configcat/evaluationdetails.h index bcbc99e..827395e 100644 --- a/include/configcat/evaluationdetails.h +++ b/include/configcat/evaluationdetails.h @@ -1,49 +1,96 @@ #pragma once -#include -#include #include +#include + #include "config.h" +#include "configcatuser.h" namespace configcat { -class ConfigCatUser; +using fetch_time_t = std::chrono::time_point>; -struct EvaluationDetails { -public: - EvaluationDetails(const std::string& key = "", - const Value& value = {}, - const std::string& variationId = "", - const std::chrono::time_point>& fetchTime = {}, - const ConfigCatUser* user = nullptr, - bool isDefaultValue = false, - const std::string& error = "", - const RolloutRule* matchedEvaluationRule = nullptr, - const RolloutPercentageItem* matchedEvaluationPercentageRule = nullptr) +struct EvaluationDetailsBase { + std::string key; + std::optional variationId; + configcat::fetch_time_t fetchTime; + std::shared_ptr user; + bool isDefaultValue; + std::optional errorMessage; + std::exception_ptr errorException; + std::optional matchedTargetingRule; + std::optional matchedPercentageOption; + + inline std::optional value() const { return getValue(); } + +protected: + EvaluationDetailsBase(const std::string& key = "", + const std::optional& variationId = "", + const configcat::fetch_time_t& fetchTime = {}, + const std::shared_ptr& user = nullptr, + bool isDefaultValue = false, + const std::optional& errorMessage = std::nullopt, + const std::exception_ptr& errorException = nullptr, + const TargetingRule* matchedTargetingRule = nullptr, + const PercentageOption* matchedPercentageOption = nullptr) : key(key) - , value(value) , variationId(variationId) , fetchTime(fetchTime) , user(user) , isDefaultValue(isDefaultValue) - , error(error) - , matchedEvaluationRule(matchedEvaluationRule ? std::optional{*matchedEvaluationRule} : std::nullopt) - , matchedEvaluationPercentageRule(matchedEvaluationPercentageRule ? std::optional{*matchedEvaluationPercentageRule} : std::nullopt) + , errorMessage(errorMessage) + , errorException(errorException) + // Unfortunately, std::optional is not possible (https://stackoverflow.com/a/26862721/8656352). + // We could use std::optional> as a workaround. However, that would take up more space + // than pointers, so we'd rather resort to pointers, as this is ctor is not meant for public use. + , matchedTargetingRule(matchedTargetingRule ? std::optional(*matchedTargetingRule) : std::nullopt) + , matchedPercentageOption(matchedPercentageOption ? std::optional(*matchedPercentageOption) : std::nullopt) {} - static EvaluationDetails fromError(const std::string& key, const Value& value, const std::string& error, const std::string& variationId = {}) { - return EvaluationDetails(key, value, variationId, {}, nullptr, true, error); + virtual std::optional getValue() const = 0; +}; + +template > +struct EvaluationDetails : public EvaluationDetailsBase { + EvaluationDetails(const std::string& key = "", + const ValueType& value = {}, + const std::optional& variationId = "", + const configcat::fetch_time_t& fetchTime = {}, + const std::shared_ptr& user = nullptr, + bool isDefaultValue = false, + const std::optional& errorMessage = std::nullopt, + const std::exception_ptr& errorException = nullptr, + const TargetingRule* matchedTargetingRule = nullptr, + const PercentageOption* matchedPercentageOption = nullptr) : + EvaluationDetailsBase(key, variationId, fetchTime, user, isDefaultValue, errorMessage, errorException, matchedTargetingRule, matchedPercentageOption), + value(value) { } - std::string key; - Value value; - std::string variationId; - std::chrono::time_point> fetchTime; - const ConfigCatUser* user; - bool isDefaultValue; - std::string error; - std::optional matchedEvaluationRule; - std::optional matchedEvaluationPercentageRule; + static EvaluationDetails fromError(const std::string& key, + const ValueType& defaultValue, + const std::string& errorMessage, + const std::exception_ptr& errorException = nullptr) { + return EvaluationDetails(key, defaultValue, std::nullopt, {}, nullptr, true, errorMessage, errorException); + } + + ValueType value; + +protected: + std::optional getValue() const override { + if constexpr (std::is_same_v>) { + return value; + } else { + return Value(value); + } + } }; +/** Helper function for creating copies of [EvaluationDetailsBase], which is not constructible, thus, not copyable. */ +inline EvaluationDetails<> to_concrete(const EvaluationDetailsBase& details) { + return EvaluationDetails<>(details.key, details.value(), details.variationId, details.fetchTime, + details.user, details.isDefaultValue, details.errorMessage, details.errorException, + details.matchedTargetingRule ? &*details.matchedTargetingRule : nullptr, + details.matchedPercentageOption ? &*details.matchedPercentageOption : nullptr); +} + } // namespace configcat diff --git a/include/configcat/fileoverridedatasource.h b/include/configcat/fileoverridedatasource.h index 067fcc1..debde25 100644 --- a/include/configcat/fileoverridedatasource.h +++ b/include/configcat/fileoverridedatasource.h @@ -10,7 +10,9 @@ namespace configcat { class FileFlagOverrides : public FlagOverrides { public: FileFlagOverrides(const std::string& filePath, OverrideBehaviour behaviour); - std::shared_ptr createDataSource(std::shared_ptr logger) override; + std::shared_ptr createDataSource(const std::shared_ptr& logger) override; + + inline OverrideBehaviour getBehavior() override { return behaviour; } private: const std::string filePath; @@ -20,10 +22,10 @@ class FileFlagOverrides : public FlagOverrides { class FileOverrideDataSource : public OverrideDataSource { public: - FileOverrideDataSource(const std::string& filePath, OverrideBehaviour behaviour, std::shared_ptr logger); + FileOverrideDataSource(const std::string& filePath, OverrideBehaviour behaviour, const std::shared_ptr& logger); // Gets all the overrides defined in the given source. - const std::shared_ptr getOverrides() override; + std::shared_ptr getOverrides() override; private: void reloadFileContent(); diff --git a/include/configcat/flagoverrides.h b/include/configcat/flagoverrides.h index 1265a15..5027780 100644 --- a/include/configcat/flagoverrides.h +++ b/include/configcat/flagoverrides.h @@ -25,8 +25,10 @@ enum OverrideBehaviour { // FlagOverrides abstract base class. class FlagOverrides { public: - virtual std::shared_ptr createDataSource(std::shared_ptr logger) = 0; + virtual std::shared_ptr createDataSource(const std::shared_ptr& logger) = 0; virtual ~FlagOverrides() = default; + + virtual OverrideBehaviour getBehavior() = 0; }; } // namespace configcat diff --git a/include/configcat/httpsessionadapter.h b/include/configcat/httpsessionadapter.h index 13d0937..1dc8b95 100644 --- a/include/configcat/httpsessionadapter.h +++ b/include/configcat/httpsessionadapter.h @@ -6,12 +6,19 @@ namespace configcat { +enum class ResponseErrorCode : int { + OK = 0, + TimedOut = 1, + RequestCancelled = 2, + InternalError = 3 +}; + struct Response { long statusCode = 0; std::string text; std::map header; - bool operationTimedOut = false; + ResponseErrorCode errorCode = ResponseErrorCode::OK; std::string error; }; diff --git a/include/configcat/keyvalue.h b/include/configcat/keyvalue.h index ca0231e..0fc0110 100644 --- a/include/configcat/keyvalue.h +++ b/include/configcat/keyvalue.h @@ -6,9 +6,14 @@ namespace configcat { struct KeyValue { - KeyValue(const std::string& key, const Value& value): - key(key), - value(value) { + KeyValue(const std::string& key, const Value& value) + : key(key) + , value(value) { + } + + KeyValue(const std::string& key, Value&& value) noexcept + : key(key) + , value(value) { } std::string key; diff --git a/include/configcat/log.h b/include/configcat/log.h index a292149..e531618 100644 --- a/include/configcat/log.h +++ b/include/configcat/log.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include @@ -7,7 +9,6 @@ namespace configcat { class ConfigCatUser; -struct Value; enum LogLevel { LOG_LEVEL_ERROR, @@ -18,10 +19,10 @@ enum LogLevel { inline const char* logLevelAsString(LogLevel level) { static const char* const names[] = { - "Error", - "Warning", - "Info", - "Debug" + "ERROR", + "WARNING", + "INFO", + "DEBUG" }; return (LOG_LEVEL_ERROR <= level && level <= LOG_LEVEL_DEBUG) ? names[level] : ""; } @@ -32,10 +33,22 @@ class ILogger { void setLogLevel(LogLevel logLevel) { maxLogLevel = logLevel; } LogLevel getLogLevel() const { return maxLogLevel; } - virtual void log(LogLevel level, const std::string& message) = 0; + virtual void log(LogLevel level, const std::string& message, const std::exception_ptr& exception = nullptr) = 0; protected: LogLevel maxLogLevel = LOG_LEVEL_WARNING; }; } // namespace configcat + +static inline std::string unwrap_exception_message(const std::exception_ptr& eptr) { + // Based on: https://stackoverflow.com/a/37222762/8656352 + if (eptr) { + try { std::rethrow_exception(eptr); } + catch (const std::exception& ex) { return ex.what(); } + catch (const std::string& ex) { return ex; } + catch (const char* ex) { return ex; } + catch (...) { return ""; } + } + return ""; +} diff --git a/include/configcat/mapoverridedatasource.h b/include/configcat/mapoverridedatasource.h index 1d2d5d5..6d55f63 100644 --- a/include/configcat/mapoverridedatasource.h +++ b/include/configcat/mapoverridedatasource.h @@ -8,7 +8,9 @@ namespace configcat { class MapFlagOverrides : public FlagOverrides { public: MapFlagOverrides(const std::unordered_map& source, OverrideBehaviour behaviour); - std::shared_ptr createDataSource(std::shared_ptr logger) override; + std::shared_ptr createDataSource(const std::shared_ptr& logger) override; + + inline OverrideBehaviour getBehavior() override { return behaviour; } private: const std::shared_ptr overrides; @@ -17,17 +19,17 @@ class MapFlagOverrides : public FlagOverrides { class MapOverrideDataSource : public OverrideDataSource { public: - MapOverrideDataSource(const std::shared_ptr overrides, + MapOverrideDataSource(const std::shared_ptr& overrides, OverrideBehaviour behaviour): OverrideDataSource(behaviour), overrides(overrides) { } // Gets all the overrides defined in the given source. - const std::shared_ptr getOverrides() override { return overrides; } + std::shared_ptr getOverrides() override { return overrides; } private: - const std::shared_ptr overrides; + std::shared_ptr overrides; }; } // namespace configcat diff --git a/include/configcat/overridedatasource.h b/include/configcat/overridedatasource.h index 7220e58..5d130ad 100644 --- a/include/configcat/overridedatasource.h +++ b/include/configcat/overridedatasource.h @@ -17,7 +17,7 @@ class OverrideDataSource { OverrideBehaviour getBehaviour() const { return behaviour; } // Gets all the overrides defined in the given source. - virtual const std::shared_ptr getOverrides() = 0; + virtual std::shared_ptr getOverrides() = 0; private: OverrideBehaviour behaviour; diff --git a/include/configcat/pollingmode.h b/include/configcat/pollingmode.h index f6f1fc0..a8663c0 100644 --- a/include/configcat/pollingmode.h +++ b/include/configcat/pollingmode.h @@ -7,9 +7,7 @@ namespace configcat { -class RefreshPolicy; class ConfigFetcher; -class ConfigJsonCache; // The base class of a polling mode configuration. class PollingMode { diff --git a/include/configcat/refreshresult.h b/include/configcat/refreshresult.h index f2809a5..2370e37 100644 --- a/include/configcat/refreshresult.h +++ b/include/configcat/refreshresult.h @@ -1,12 +1,15 @@ #pragma once +#include +#include #include namespace configcat { struct RefreshResult { - bool success = false; - std::string error; + inline bool success() { return !errorMessage; }; + std::optional errorMessage; + std::exception_ptr errorException; }; } // namespace configcat diff --git a/include/configcat/timeutils.h b/include/configcat/timeutils.h new file mode 100644 index 0000000..834662a --- /dev/null +++ b/include/configcat/timeutils.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace configcat { + +using date_time_t = std::chrono::system_clock::time_point; + +std::string datetime_to_isostring(const date_time_t& tp); +date_time_t make_datetime(int year, int month, int day, int hour, int min, int sec, int millisec = 0); + +} // namespace configcat diff --git a/src/config.cpp b/src/config.cpp index 20995cd..f867de5 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1,17 +1,17 @@ -#include "configcat/config.h" -#include #include -#include +#include + +#include "configcat/config.h" +#include "utils.h" using namespace std; using json = nlohmann::json; -// nlohmann::json std::shared_ptr serialization - namespace nlohmann { + // nlohmann::json std::optional serialization template - struct adl_serializer> { - static void to_json(json& j, const std::shared_ptr& opt) { + struct adl_serializer> { + static void to_json(json& j, const optional& opt) { if (opt) { j = *opt; } else { @@ -19,190 +19,417 @@ namespace nlohmann { } } - static void from_json(const json& j, std::shared_ptr& opt) { + static void from_json(const json& j, optional& opt) { if (j.is_null()) { - opt = nullptr; + opt = nullopt; } else { - opt.reset(new T(j.get())); + opt = optional{ j.get() }; } } }; -} // namespace nlohmann +} // namespace nlohmann namespace configcat { -string valueToString(const ValueType& value) { - return visit([](auto&& arg){ - using T = decay_t; - if constexpr (is_same_v) { - return arg; - } else if constexpr (is_same_v) { - return string(arg); - } else if constexpr (is_same_v) { - return string(arg ? "true" : "false"); +Value::operator SettingValue() const { + return visit([](auto&& alt) -> SettingValue { + return alt; + }, *this); +} + +string Value::toString() const { + return visit([](auto&& alt) -> string { + using T = decay_t; + if constexpr (is_same_v) { + return alt ? "true" : "false"; + } else if constexpr (is_same_v) { + return alt; + } else if constexpr (is_same_v) { + return string_format("%d", alt); } else if constexpr (is_same_v) { - auto str = to_string(arg); - // Drop unnecessary '0' characters at the end of the string and keep format 0.0 for zero double - auto pos = str.find_last_not_of('0'); - if (pos != string::npos && str[pos] == '.') { - ++pos; - } - return str.erase(pos + 1, string::npos); + return number_to_string(alt); } else { - return to_string(arg); + static_assert(always_false_v, "Non-exhaustive visitor."); } - }, value); + }, *this); } // Config serialization -void to_json(json& j, const ValueType& value) { - std::visit([&](auto&& arg){j = arg;}, value); +#pragma region SettingValue + +struct SettingValuePrivate { + static void setUnsupportedValue(SettingValue& value, const json& j) { + value = nullopt; + value.unsupportedValue = shared_ptr(new SettingValue::UnsupportedValue{ j.type_name(), j.dump() }); + } +}; + +void to_json(json& j, const SettingValue& value) { + if (holds_alternative(value)) { + j[SettingValue::kBoolean] = get(value); + } else if (holds_alternative(value)) { + j[SettingValue::kString] = get(value); + } else if (holds_alternative(value)) { + j[SettingValue::kInt] = get(value); + } else if (holds_alternative(value)) { + j[SettingValue::kDouble] = get(value); + } } -void from_json(const json& j, ValueType& value) { - if (j.is_boolean()) { - value = j.get(); - } else if (j.is_string()) { - value = j.get(); - } else if (j.is_number_integer()) { - value = j.get(); - } else if (j.is_number_float()) { - value = j.get(); - } else { - throw json::parse_error::create(105, 0, string("Invalid value type: ") + j.type_name(), &j); +void from_json(const json& j, SettingValue& value) { + auto valueFound = false; + if (auto it = j.find(SettingValue::kBoolean); it != j.end()) { + value = it->get(); + valueFound = true; + } + if (auto it = j.find(SettingValue::kString); it != j.end()) { + if (value = it->get(); valueFound) value = nullopt; + else valueFound = true; + } + if (auto it = j.find(SettingValue::kInt); it != j.end()) { + if (value = it->get(); valueFound) value = nullopt; + else valueFound = true; + } + if (auto it = j.find(SettingValue::kDouble); it != j.end()) { + if (value = it->get(); valueFound) value = nullopt; + else valueFound = true; + } + if (!valueFound) { + SettingValuePrivate::setUnsupportedValue(value, j); } } -void to_json(json& j, const Preferences& preferences) { - j[Config::kPreferencesUrl] = preferences.url; - j[Config::kPreferencesRedirect] = preferences.redirect; +SettingValue::operator optional() const { + return visit([](auto&& alt) -> optional { + using T = decay_t; + if constexpr (is_same_v) { + return nullopt; + } else { + return alt; + } + }, *this); } -void from_json(const json& j, Preferences& preferences) { - j.at(Config::kPreferencesUrl).get_to(preferences.url); - j.at(Config::kPreferencesRedirect).get_to(preferences.redirect); +optional SettingValue::toValueChecked(SettingType type, bool throwIfInvalid) const +{ + return visit([&](auto&& alt) -> optional { + using T = decay_t; + + if constexpr (is_same_v) { + if (type == SettingType::Boolean) return alt; + } else if constexpr (is_same_v) { + if (type == SettingType::String) return alt; + } else if constexpr (is_same_v) { + if (type == SettingType::Int) return alt; + } else if constexpr (is_same_v) { + if (type == SettingType::Double) return alt; + } else if constexpr (is_same_v) { + if (throwIfInvalid) { + throw runtime_error(unsupportedValue->type == "null" + ? "Setting value is null." + : string_format("Setting value '%s' is of an unsupported type (%s).", unsupportedValue->value.c_str(), unsupportedValue->type.c_str())); + } + return nullopt; + } else { + static_assert(always_false_v, "Non-exhaustive visitor."); + } + + if (throwIfInvalid) { + throw runtime_error("Setting value is missing or invalid."); + } + + return nullopt; + }, *this); +} + +#pragma endregion + +#pragma region SettingValueContainer + +void to_json(json& j, const SettingValueContainer& container) { + if (!holds_alternative(container.value)) j[SettingValueContainer::kValue] = container.value; + if (container.variationId) j[SettingValueContainer::kVariationId] = container.variationId; +} + +void from_json(const json& j, SettingValueContainer& container) { + if (auto it = j.find(SettingValueContainer::kValue); it != j.end()) it->get_to(container.value); + if (auto it = j.find(SettingValueContainer::kVariationId); it != j.end()) it->get_to(container.variationId); +} + +#pragma endregion + +#pragma region PercentageOption + +void to_json(json& j, const PercentageOption& percentageOption) { + j[PercentageOption::kPercentage] = percentageOption.percentage; + to_json(j, static_cast(percentageOption)); +} + +void from_json(const json& j, PercentageOption& percentageOption) { + j.at(PercentageOption::kPercentage).get_to(percentageOption.percentage); + from_json(j, static_cast(percentageOption)); +} + +#pragma endregion + +#pragma region UserCondition + +void to_json(json& j, const UserCondition& condition) { + j[UserCondition::kComparisonAttribute] = condition.comparisonAttribute; + j[UserCondition::kComparator] = condition.comparator; + if (holds_alternative(condition.comparisonValue)) { + j[UserCondition::kStringComparisonValue] = get(condition.comparisonValue); + } else if (holds_alternative(condition.comparisonValue)) { + j[UserCondition::kNumberComparisonValue] = get(condition.comparisonValue); + } else if (holds_alternative>(condition.comparisonValue)) { + j[UserCondition::kStringListComparisonValue] = get>(condition.comparisonValue); + } +} + +void from_json(const json& j, UserCondition& condition) { + j.at(UserCondition::kComparisonAttribute).get_to(condition.comparisonAttribute); + j.at(UserCondition::kComparator).get_to(condition.comparator); + auto comparisonValueFound = false; + if (auto it = j.find(UserCondition::kStringComparisonValue); it != j.end()) { + condition.comparisonValue = it->get(); + comparisonValueFound = true; + } + if (auto it = j.find(UserCondition::kNumberComparisonValue); it != j.end()) { + if (condition.comparisonValue = it->get(); comparisonValueFound) condition.comparisonValue = nullopt; + else comparisonValueFound = true; + } + if (auto it = j.find(UserCondition::kStringListComparisonValue); it != j.end()) { + if (condition.comparisonValue = it->get>(); comparisonValueFound) condition.comparisonValue = nullopt; + } } -void to_json(json& j, const RolloutPercentageItem& rolloutPercentageItem) { - j[Config::kValue] = rolloutPercentageItem.value; - j[Config::kPercentage] = rolloutPercentageItem.percentage; - if (!rolloutPercentageItem.variationId.empty()) j[Config::kVariationId] = rolloutPercentageItem.variationId; +#pragma endregion + +#pragma region PrerequisiteFlagCondition + +void to_json(json& j, const PrerequisiteFlagCondition& condition) { + j[PrerequisiteFlagCondition::kPrerequisiteFlagKey] = condition.prerequisiteFlagKey; + j[PrerequisiteFlagCondition::kComparator] = condition.comparator; + if (!holds_alternative(condition.comparisonValue)) j[PrerequisiteFlagCondition::kComparisonValue] = condition.comparisonValue; } -void from_json(const json& j, RolloutPercentageItem& rolloutPercentageItem) { - j.at(Config::kValue).get_to(rolloutPercentageItem.value); - j.at(Config::kPercentage).get_to(rolloutPercentageItem.percentage); - if (auto it = j.find(Config::kVariationId); it != j.end()) it->get_to(rolloutPercentageItem.variationId); +void from_json(const json& j, PrerequisiteFlagCondition& condition) { + j.at(PrerequisiteFlagCondition::kPrerequisiteFlagKey).get_to(condition.prerequisiteFlagKey); + j.at(PrerequisiteFlagCondition::kComparator).get_to(condition.comparator); + if (auto it = j.find(PrerequisiteFlagCondition::kComparisonValue); it != j.end()) it->get_to(condition.comparisonValue); } -void to_json(json& j, const RolloutRule& rolloutRule) { - j[Config::kValue] = rolloutRule.value; - j[Config::kComparisonAttribute] = rolloutRule.comparisonAttribute; - j[Config::kComparator] = rolloutRule.comparator; - j[Config::kComparisonValue] = rolloutRule.comparisonValue; - if (!rolloutRule.variationId.empty()) j[Config::kVariationId] = rolloutRule.variationId; +#pragma endregion + +#pragma region SegmentCondition + +void to_json(json& j, const SegmentCondition& condition) { + j[SegmentCondition::kSegmentIndex] = condition.segmentIndex; + j[SegmentCondition::kComparator] = condition.comparator; } -void from_json(const json& j, RolloutRule& rolloutRule) { - j.at(Config::kValue).get_to(rolloutRule.value); - j.at(Config::kComparisonAttribute).get_to(rolloutRule.comparisonAttribute); - j.at(Config::kComparator).get_to(rolloutRule.comparator); - j.at(Config::kComparisonValue).get_to(rolloutRule.comparisonValue); - if (auto it = j.find(Config::kVariationId); it != j.end()) it->get_to(rolloutRule.variationId); +void from_json(const json& j, SegmentCondition& condition) { + j.at(SegmentCondition::kSegmentIndex).get_to(condition.segmentIndex); + j.at(SegmentCondition::kComparator).get_to(condition.comparator); } +#pragma endregion + +#pragma region ConditionContainer + +void to_json(json& j, const ConditionContainer& container) { + if (holds_alternative(container.condition)) { + j[ConditionContainer::kUserCondition] = get(container.condition); + } else if (holds_alternative(container.condition)) { + j[ConditionContainer::kPrerequisiteFlagCondition] = get(container.condition); + } else if (holds_alternative(container.condition)) { + j[ConditionContainer::kSegmentCondition] = get(container.condition); + } +} + +void from_json(const json& j, ConditionContainer& container) { + auto conditionFound = false; + if (auto it = j.find(ConditionContainer::kUserCondition); it != j.end()) { + container.condition = it->get(); + conditionFound = true; + } + if (auto it = j.find(ConditionContainer::kPrerequisiteFlagCondition); it != j.end()) { + if (container.condition = it->get(); conditionFound) container.condition = nullopt; + else conditionFound = true; + } + if (auto it = j.find(ConditionContainer::kSegmentCondition); it != j.end()) { + if (container.condition = it->get(); conditionFound) container.condition = nullopt; + } +} + +#pragma endregion + +#pragma region TargetingRule + +void to_json(json& j, const TargetingRule& targetingRule) { + if (!targetingRule.conditions.empty()) j[TargetingRule::kConditions] = targetingRule.conditions; + if (holds_alternative(targetingRule.then)) { + j[TargetingRule::kSimpleValue] = get(targetingRule.then); + } else { + j[TargetingRule::kPercentageOptions] = get>(targetingRule.then); + } +} + +void from_json(const json& j, TargetingRule& targetingRule) { + if (auto it = j.find(TargetingRule::kConditions); it != j.end()) it->get_to(targetingRule.conditions); + auto thenFound = false; + if (auto it = j.find(TargetingRule::kSimpleValue); it != j.end()) { + targetingRule.then = it->get(); + thenFound = true; + } + if (auto it = j.find(TargetingRule::kPercentageOptions); it != j.end()) { + if (targetingRule.then = it->get>(); thenFound) targetingRule.then = nullopt; + } +} + +#pragma endregion + +#pragma region Segment + +void to_json(json& j, const Segment& segment) { + j[Segment::kName] = segment.name; + if (!segment.conditions.empty()) j[Segment::kConditions] = segment.conditions; +} + +void from_json(const json& j, Segment& segment) { + j.at(Segment::kName).get_to(segment.name); + if (auto it = j.find(Segment::kConditions); it != j.end()) it->get_to(segment.conditions); +} + +#pragma endregion + +#pragma region Setting + void to_json(json& j, const Setting& setting) { - j[Config::kValue] = setting.value; - if (!setting.percentageItems.empty()) j[Config::kRolloutPercentageItems] = setting.percentageItems; - if (!setting.rolloutRules.empty()) j[Config::kRolloutRules] = setting.rolloutRules; - if (!setting.variationId.empty()) j[Config::kVariationId] = setting.variationId; + j[Setting::kType] = setting.type; + if (setting.percentageOptionsAttribute) j[Setting::kPercentageOptionsAttribute] = setting.percentageOptionsAttribute; + if (!setting.targetingRules.empty()) j[Setting::kTargetingRules] = setting.targetingRules; + if (!setting.percentageOptions.empty()) j[Setting::kPercentageOptions] = setting.percentageOptions; + to_json(j, static_cast(setting)); } void from_json(const json& j, Setting& setting) { - j.at(Config::kValue).get_to(setting.value); - if (auto it = j.find(Config::kRolloutPercentageItems); it != j.end()) it->get_to(setting.percentageItems); - if (auto it = j.find(Config::kRolloutRules); it != j.end()) it->get_to(setting.rolloutRules); - if (auto it = j.find(Config::kVariationId); it != j.end()) it->get_to(setting.variationId); + j.at(Setting::kType).get_to(setting.type); + if (auto it = j.find(Setting::kPercentageOptionsAttribute); it != j.end()) it->get_to(setting.percentageOptionsAttribute); + if (auto it = j.find(Setting::kTargetingRules); it != j.end()) it->get_to(setting.targetingRules); + if (auto it = j.find(Setting::kPercentageOptions); it != j.end()) it->get_to(setting.percentageOptions); + from_json(j, static_cast(setting)); +} + +Setting Setting::fromValue(const SettingValue& value) { + Setting setting; + setting.type = value.getSettingType(); + setting.value = value; + return setting; +} + +SettingType Setting::getTypeChecked() const { + if (hasInvalidType()) { + throw std::runtime_error("Setting type is invalid."); + } + return type; +} + +#pragma endregion + +#pragma region Preference + +void to_json(json& j, const Preferences& preferences) { + j[Preferences::kBaseUrl] = preferences.baseUrl; + j[Preferences::kRedirectMode] = preferences.redirectMode; + if (preferences.salt) { + j[Preferences::kSalt] = *preferences.salt; + } else { + j[Preferences::kSalt] = nullptr; + } +} + +void from_json(const json& j, Preferences& preferences) { + if (auto it = j.find(Preferences::kBaseUrl); it != j.end()) it->get_to(preferences.baseUrl); + if (auto it = j.find(Preferences::kRedirectMode); it != j.end()) it->get_to(preferences.redirectMode); + if (auto it = j.find(Preferences::kSalt); it != j.end()) { + preferences.salt = make_shared(); + it->get_to(*preferences.salt); + } } -void to_json(json& j, const Config& value) { - if (value.preferences) j[Config::kPreferences] = value.preferences; - if (value.entries) j[Config::kEntries] = value.entries; +#pragma endregion + +#pragma region Config + +void to_json(json& j, const Config& config) { + j[Config::kPreferences] = config.preferences ? *config.preferences : Preferences(); + if (config.segments && !config.segments->empty()) j[Config::kSegments] = *config.segments; + if (config.settings && !config.settings->empty()) j[Config::kSettings] = *config.settings; } void from_json(const json& j, Config& config) { if (auto it = j.find(Config::kPreferences); it != j.end()) it->get_to(config.preferences); - if (auto it = j.find(Config::kEntries); it != j.end()) it->get_to(config.entries); + if (auto it = j.find(Config::kSegments); it != j.end()) it->get_to(*(config.segments = make_shared())); + if (auto it = j.find(Config::kSettings); it != j.end()) it->get_to(*(config.settings = make_shared())); } -std::shared_ptr Config::empty = std::make_shared(); +const shared_ptr Config::empty = make_shared(); -std::string Config::toJson() { +string Config::toJson() { return json(*this).dump(); } -shared_ptr Config::fromJson(const string& jsonString) { - json configObj = json::parse(jsonString); +shared_ptr Config::fromJson(const string& jsonString, bool tolerant) { + json configObj = json::parse(jsonString, nullptr, true, tolerant); // tolerant = ignore comment auto config = make_shared(); configObj.get_to(*config); + config->fixupSaltAndSegments(); return config; } -shared_ptr Config::fromFile(const string& filePath) { +shared_ptr Config::fromFile(const string& filePath, bool tolerant) { ifstream file(filePath); - json data = json::parse(file); + json data = json::parse(file, nullptr, true, tolerant); // tolerant = ignore comment auto config = make_shared(); if (auto it = data.find("flags"); it != data.end()) { // Simple (key-value) json format - config->entries = make_shared>(); + config->settings = make_shared(); for (auto& [key, value] : it->items()) { - Setting setting; - value.get_to(setting.value); - config->entries->insert({key, setting}); + SettingValue settingValue; + if (value.is_boolean()) { + settingValue = value.get(); + } else if (value.is_string()) { + settingValue = value.get(); + } else if (value.is_number_integer()) { + settingValue = value.get(); + } else if (value.is_number()) { + settingValue = value.get(); + } else { + SettingValuePrivate::setUnsupportedValue(settingValue, value); + } + config->settings->insert({ key, Setting::fromValue(settingValue) }); } } else { // Complex (full-featured) json format data.get_to(*config); + config->fixupSaltAndSegments(); } return config; } -std::shared_ptr ConfigEntry::empty = std::make_shared(Config::empty, "empty"); - -shared_ptr ConfigEntry::fromString(const string& text) { - if (text.empty()) - return ConfigEntry::empty; - - auto fetchTimeIndex = text.find('\n'); - auto eTagIndex = text.find('\n', fetchTimeIndex + 1); - if (fetchTimeIndex == string::npos || eTagIndex == string::npos) { - throw std::invalid_argument("Number of values is fewer than expected."); - } - - auto fetchTimeString = text.substr(0, fetchTimeIndex); - double fetchTime; - try { - fetchTime = std::stod(fetchTimeString); - } catch (const std::exception& e) { - throw std::invalid_argument("Invalid fetch time: " + fetchTimeString + ". " + e.what()); - } +void Config::fixupSaltAndSegments() { + if (settings && !settings->empty()) { + auto configJsonSalt = preferences ? preferences->salt : nullptr; - auto eTag = text.substr(fetchTimeIndex + 1, eTagIndex - fetchTimeIndex - 1); - if (eTag.empty()) { - throw std::invalid_argument("Empty eTag value"); - } - - auto configJsonString = text.substr(eTagIndex + 1); - try { - return make_shared(Config::fromJson(configJsonString), eTag, configJsonString, fetchTime / 1000.0); - } catch (const std::exception& e) { - throw std::invalid_argument("Invalid config JSON: " + configJsonString + ". " + e.what()); + for (auto& [_, setting] : *settings) { + setting.configJsonSalt = configJsonSalt; + setting.segments = segments; + } } } -string ConfigEntry::serialize() const { - return to_string(static_cast(floor(fetchTime * 1000))) + "\n" + eTag + "\n" + configJsonString; -} +#pragma endregion } // namespace configcat diff --git a/src/configcatclient.cpp b/src/configcatclient.cpp index 2ea5d75..4f17685 100644 --- a/src/configcatclient.cpp +++ b/src/configcatclient.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "configcat/configcatclient.h" #include "configcat/configcatuser.h" @@ -8,9 +9,8 @@ #include "configservice.h" #include "configcat/flagoverrides.h" #include "configcat/overridedatasource.h" -#include "configcat/configcatlogger.h" +#include "configcatlogger.h" #include "configcat/consolelogger.h" -#include "utils.h" using namespace std; using namespace std::chrono; @@ -18,39 +18,69 @@ using namespace std::chrono; namespace configcat { std::mutex ConfigCatClient::instancesMutex; -std::unordered_map> ConfigCatClient::instances; +std::unordered_map> ConfigCatClient::instances; -ConfigCatClient* ConfigCatClient::get(const std::string& sdkKey, const ConfigCatOptions* options) { +bool isValidSdkKey(const string& sdkKey, bool customBaseUrl) { + static constexpr char proxyPrefix[] = "configcat-proxy/"; + + if (customBaseUrl && sdkKey.size() >= sizeof(proxyPrefix) && starts_with(sdkKey, proxyPrefix)) { + return true; + } + + static const regex re("^(?:configcat-sdk-1/)?[^/]{22}/[^/]{22}$", regex_constants::ECMAScript); + return regex_match(sdkKey, re); +} + +// NOTE: make_shared doesn't work with private ctors but we can use this workaround to avoid double allocation +// (see also https://stackoverflow.com/a/8147213/8656352) +struct ConfigCatClient::MakeSharedEnabler : ConfigCatClient { + MakeSharedEnabler(const std::string& sdkKey, const ConfigCatOptions& options) : ConfigCatClient(sdkKey, options) {} +}; + +std::shared_ptr ConfigCatClient::get(const std::string& sdkKey, const ConfigCatOptions* options) { if (sdkKey.empty()) { - throw std::invalid_argument("The SDK key cannot be empty."); + throw invalid_argument("SDK Key cannot be empty."); + } + + ConfigCatOptions defaultOptions; + const auto& actualOptions = options ? *options : defaultOptions; + + const auto& flagOverrides = actualOptions.flagOverrides; + if (!flagOverrides || flagOverrides->getBehavior() != OverrideBehaviour::LocalOnly) { + const auto customBaseUrl = !actualOptions.baseUrl.empty(); + if (!isValidSdkKey(sdkKey, customBaseUrl)) { + throw invalid_argument(string_format("SDK Key '%s' is invalid.", sdkKey.c_str())); + } } lock_guard lock(instancesMutex); auto client = instances.find(sdkKey); - if (client != instances.end()) { - if (options) { - LOG_WARN_OBJECT(client->second->logger, 3000) << - "There is an existing client instance for the specified SDK Key. " - "No new client instance will be created and the specified options are ignored. " - "Returning the existing client instance. SDK Key: '" << sdkKey << "'."; - } - return client->second.get(); + if (client == instances.end()) { + client = instances.insert({sdkKey, make_shared(sdkKey, actualOptions)}).first; + } else if (options) { + LOG_WARN_OBJECT(client->second->logger, 3000) << + "There is an existing client instance for the specified SDK Key. " + "No new client instance will be created and the specified options are ignored. " + "Returning the existing client instance. SDK Key: '" << sdkKey << "'."; } + return client->second; +} - client = instances.insert({ - sdkKey, - std::move(std::unique_ptr(new ConfigCatClient(sdkKey, options ? *options : ConfigCatOptions()))) - }).first; +void ConfigCatClient::close(const std::shared_ptr& client) { + if (!client) { + return; + } - return client->second.get(); -} + { + lock_guard lock(instancesMutex); -void ConfigCatClient::close(ConfigCatClient* client) { - lock_guard lock(instancesMutex); - for (auto it = instances.begin(); it != instances.end(); ++it) { - if (it->second.get() == client) { - instances.erase(it); - return; + client->closeResources(); + + for (auto it = instances.begin(); it != instances.end(); ++it) { + if (it->second == client) { + instances.erase(it); + return; + } } } @@ -60,10 +90,17 @@ void ConfigCatClient::close(ConfigCatClient* client) { void ConfigCatClient::closeAll() { lock_guard lock(instancesMutex); + + for (const auto& [_, instance] : instances) { + instance->closeResources(); + } + instances.clear(); } size_t ConfigCatClient::instanceCount() { + lock_guard lock(instancesMutex); + return instances.size(); } @@ -86,13 +123,17 @@ ConfigCatClient::ConfigCatClient(const std::string& sdkKey, const ConfigCatOptio } } +void ConfigCatClient::closeResources() { + configService.reset(); // stop polling by destroying configService +} + SettingResult ConfigCatClient::getSettings() const { if (overrideDataSource) { switch (overrideDataSource->getBehaviour()) { case LocalOnly: return { overrideDataSource->getOverrides(), kDistantPast }; case LocalOverRemote: { - auto settingResult = configService->getSettings(); + auto settingResult = configService ? configService->getSettings() : SettingResult{nullptr, kDistantPast}; auto remote = settingResult.settings; auto local = overrideDataSource->getOverrides(); auto result = make_shared(); @@ -110,7 +151,7 @@ SettingResult ConfigCatClient::getSettings() const { return { result, settingResult.fetchTime }; } case RemoteOverLocal: - auto settingResult = configService->getSettings(); + auto settingResult = configService ? configService->getSettings() : SettingResult{nullptr, kDistantPast}; auto remote = settingResult.settings; auto local = overrideDataSource->getOverrides(); auto result = make_shared(); @@ -129,267 +170,355 @@ SettingResult ConfigCatClient::getSettings() const { } } - return configService->getSettings(); + return configService ? configService->getSettings() : SettingResult{nullptr, kDistantPast}; } -bool ConfigCatClient::getValue(const std::string& key, bool defaultValue, const ConfigCatUser* user) const { +bool ConfigCatClient::getValue(const std::string& key, bool defaultValue, const std::shared_ptr& user) const { return _getValue(key, defaultValue, user); } -int ConfigCatClient::getValue(const std::string& key, int defaultValue, const ConfigCatUser* user) const { +int32_t ConfigCatClient::getValue(const std::string& key, int32_t defaultValue, const std::shared_ptr& user) const { return _getValue(key, defaultValue, user); } -double ConfigCatClient::getValue(const std::string& key, double defaultValue, const ConfigCatUser* user) const { +double ConfigCatClient::getValue(const std::string& key, double defaultValue, const std::shared_ptr& user) const { return _getValue(key, defaultValue, user); } -std::string ConfigCatClient::getValue(const std::string& key, const char* defaultValue, const ConfigCatUser* user) const { - return _getValue(key, string(defaultValue), user); +std::string ConfigCatClient::getValue(const std::string& key, const char* defaultValue, const std::shared_ptr& user) const { + return _getValue(key, defaultValue, user); } -std::string ConfigCatClient::getValue(const std::string& key, const std::string& defaultValue, const ConfigCatUser* user) const { +std::string ConfigCatClient::getValue(const std::string& key, const std::string& defaultValue, const std::shared_ptr& user) const { return _getValue(key, defaultValue, user); } -std::shared_ptr ConfigCatClient::getValue(const std::string& key, const ConfigCatUser* user) const { - auto settingResult = getSettings(); - auto& settings = settingResult.settings; - auto& fetchTime = settingResult.fetchTime; - if (!settings) { - LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1000); - logEntry << "Config JSON is not present when evaluating setting '" << key << "'. Returning nullptr."; - hooks->invokeOnFlagEvaluated(EvaluationDetails::fromError(key, {}, logEntry.getMessage())); - return {}; - } - - auto setting = settings->find(key); - if (setting == settings->end()) { - vector keys; - keys.reserve(settings->size()); - for (auto keyValue : *settings) { - keys.emplace_back("'" + keyValue.first + "'"); - } - LOG_ERROR(1001) << - "Failed to evaluate setting '" << key << "' (the key was not found in config JSON). " - "Returning nullptr. Available keys: " << keys << "."; - return {}; - } - - auto details = evaluate(key, user, setting->second, fetchTime); - return make_shared(details.value); +std::optional ConfigCatClient::getValue(const std::string& key, const std::shared_ptr& user) const { + return _getValue>(key, nullopt, user); } -EvaluationDetails ConfigCatClient::getValueDetails(const std::string& key, bool defaultValue, const ConfigCatUser* user) const { +EvaluationDetails ConfigCatClient::getValueDetails(const std::string& key, bool defaultValue, const std::shared_ptr& user) const { return _getValueDetails(key, defaultValue, user); } -EvaluationDetails ConfigCatClient::getValueDetails(const std::string& key, int defaultValue, const ConfigCatUser* user) const { +EvaluationDetails ConfigCatClient::getValueDetails(const std::string& key, int32_t defaultValue, const std::shared_ptr& user) const { return _getValueDetails(key, defaultValue, user); } -EvaluationDetails ConfigCatClient::getValueDetails(const std::string& key, double defaultValue, const ConfigCatUser* user) const { +EvaluationDetails ConfigCatClient::getValueDetails(const std::string& key, double defaultValue, const std::shared_ptr& user) const { return _getValueDetails(key, defaultValue, user); } -EvaluationDetails ConfigCatClient::getValueDetails(const std::string& key, const std::string& defaultValue, const ConfigCatUser* user) const { +EvaluationDetails ConfigCatClient::getValueDetails(const std::string& key, const std::string& defaultValue, const std::shared_ptr& user) const { return _getValueDetails(key, defaultValue, user); } -EvaluationDetails ConfigCatClient::getValueDetails(const std::string& key, const char* defaultValue, const ConfigCatUser* user) const { - return _getValueDetails(key, defaultValue, user); +EvaluationDetails ConfigCatClient::getValueDetails(const std::string& key, const char* defaultValue, const std::shared_ptr& user) const { + return _getValueDetails(key, defaultValue, user); +} + +EvaluationDetails> ConfigCatClient::getValueDetails(const std::string& key, const std::shared_ptr& user) const { + return _getValueDetails>(key, nullopt, user); } template -EvaluationDetails ConfigCatClient::_getValueDetails(const std::string& key, ValueType defaultValue, const ConfigCatUser* user) const { - auto settingResult = getSettings(); - auto& settings = settingResult.settings; - auto& fetchTime = settingResult.fetchTime; - if (!settings) { - LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1000); - logEntry << "Config JSON is not present when evaluating setting '" << key << "'. Returning the `defaultValue` parameter that you specified in your application: '" << defaultValue << "'."; - auto details = EvaluationDetails::fromError(key, defaultValue, logEntry.getMessage()); - hooks->invokeOnFlagEvaluated(details); - return details; - } +EvaluationDetails ConfigCatClient::_getValueDetails(const std::string& key, const ValueType& defaultValue, const std::shared_ptr& user) const { + try { + auto settingResult = getSettings(); + auto& settings = settingResult.settings; + auto& fetchTime = settingResult.fetchTime; + if (!settings) { + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1000); + if constexpr (is_same_v>) { + logEntry << "Config JSON is not present when evaluating setting '" << key << "'. Returning std::nullopt."; + } else { + logEntry << "Config JSON is not present when evaluating setting '" << key << "'. Returning the `defaultValue` parameter that you specified in your application: '" << defaultValue << "'."; + } + auto details = EvaluationDetails::fromError(key, defaultValue, logEntry.getMessage()); + hooks->invokeOnFlagEvaluated(details); + return details; + } - auto setting = settings->find(key); - if (setting == settings->end()) { - vector keys; - keys.reserve(settings->size()); - for (auto keyValue : *settings) { - keys.emplace_back("'" + keyValue.first + "'"); + auto setting = settings->find(key); + if (setting == settings->end()) { + vector keys; + keys.reserve(settings->size()); + for (const auto& [key, _] : *settings) { + keys.emplace_back(key); + } + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1001); + if constexpr (is_same_v>) { + logEntry << + "Failed to evaluate setting '" << key << "' (the key was not found in config JSON). " + "Returning the `defaultValue` parameter that you specified in your application: '" << defaultValue << "'. " + "Available keys: " << keys << "."; + } else { + logEntry << + "Failed to evaluate setting '" << key << "' (the key was not found in config JSON). " + "Returning std::nullopt. Available keys: " << keys << "."; + } + auto details = EvaluationDetails::fromError(key, defaultValue, logEntry.getMessage()); + hooks->invokeOnFlagEvaluated(details); + return details; } - LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1001); - logEntry << - "Failed to evaluate setting '" << key << "' (the key was not found in config JSON). " - "Returning the `defaultValue` parameter that you specified in your application: '" << defaultValue << "'. " - "Available keys: " << keys << "."; - auto details = EvaluationDetails::fromError(key, defaultValue, logEntry.getMessage()); + + const auto& effectiveUser = user ? user : defaultUser; + return evaluate(key, defaultValue, effectiveUser, setting->second, settings, fetchTime); + } + catch (...) { + auto ex = std::current_exception(); + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1002, ex); + logEntry << "Error occurred in the `getValueDetails` method while evaluating setting '" << key << "'. "; + if constexpr (is_same_v>) { + logEntry << "Returning std::nullopt."; + } else { + logEntry << "Returning the `defaultValue` parameter that you specified in your application: '" << defaultValue << "'."; + } + auto details = EvaluationDetails::fromError(key, defaultValue, logEntry.getMessage(), ex); hooks->invokeOnFlagEvaluated(details); return details; } - - return evaluate(key, user, setting->second, fetchTime); } std::vector ConfigCatClient::getAllKeys() const { - auto settingResult = getSettings(); - auto& settings = settingResult.settings; - auto& fetchTime = settingResult.fetchTime; - if (!settings) { - LOG_ERROR(1000) << "Config JSON is not present. Returning empty list."; - return {}; - } + try { + auto settingResult = getSettings(); + auto& settings = settingResult.settings; + auto& fetchTime = settingResult.fetchTime; + if (!settings) { + LOG_ERROR(1000) << "Config JSON is not present. Returning empty list."; + return {}; + } - vector keys; - keys.reserve(settings->size()); - for (auto keyValue : *settings) { - keys.emplace_back(keyValue.first); + vector keys; + keys.reserve(settings->size()); + for (const auto& [key, _] : *settings) { + keys.emplace_back(key); + } + return keys; } - return keys; -} - -std::shared_ptr ConfigCatClient::getKeyAndValue(const std::string& variationId) const { - auto settingResult = getSettings(); - auto& settings = settingResult.settings; - auto& fetchTime = settingResult.fetchTime; - if (!settings) { - LOG_ERROR(1000) << "Config JSON is not present. Returning null."; - return nullptr; + catch (...) { + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1002, std::current_exception()); + logEntry << "Error occurred in the `getAllKeys` method. Returning empty list."; + return {}; } +} - for (auto keyValue : *settings) { - auto& key = keyValue.first; - auto setting = keyValue.second; - if (setting.variationId == variationId) { - return make_shared(key, setting.value); +std::optional ConfigCatClient::getKeyAndValue(const std::string& variationId) const { + try { + auto settingResult = getSettings(); + auto& settings = settingResult.settings; + if (!settings) { + LOG_ERROR(1000) << "Config JSON is not present. Returning std::nullopt."; + return nullopt; } - for (auto rolloutRule : setting.rolloutRules) { - if (rolloutRule.variationId == variationId) { - return make_shared(key, rolloutRule.value); + for (const auto& [key, setting] : *settings) { + const auto settingType = setting.getTypeChecked(); + + if (setting.variationId == variationId) { + return KeyValue(key, *setting.value.toValueChecked(settingType)); + } + + for (const auto& targetingRule : setting.targetingRules) { + if (const auto simpleValuePtr = get_if(&targetingRule.then); simpleValuePtr) { + if (simpleValuePtr->variationId == variationId) { + return KeyValue(key, *simpleValuePtr->value.toValueChecked(settingType)); + } + } else if (const auto percentageOptionsPtr = get_if(&targetingRule.then); + percentageOptionsPtr && !percentageOptionsPtr->empty()) { + + for (const auto& percentageOption : *percentageOptionsPtr) { + if (percentageOption.variationId == variationId) { + return KeyValue(key, *percentageOption.value.toValueChecked(settingType)); + } + } + } else { + throw runtime_error("Targeting rule THEN part is missing or invalid."); + } } - } - for (auto percentageRule : setting.percentageItems) { - if (percentageRule.variationId == variationId) { - return make_shared(key, percentageRule.value); + for (const auto& percentageOption : setting.percentageOptions) { + if (percentageOption.variationId == variationId) { + return KeyValue(key, *percentageOption.value.toValueChecked(settingType)); + } } } - } - LOG_ERROR(2011) << "Could not find the setting for the specified variation ID: '" << variationId << "'."; - return nullptr; + LOG_ERROR(2011) << "Could not find the setting for the specified variation ID: '" << variationId << "'."; + return std::nullopt; + } + catch (...) { + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1002, std::current_exception()); + logEntry << "Error occurred in the `getKeyAndValue` method. Returning std::nullopt."; + return std::nullopt; + } } -std::unordered_map ConfigCatClient::getAllValues(const ConfigCatUser* user) const { - auto settingResult = getSettings(); - auto& settings = settingResult.settings; - auto& fetchTime = settingResult.fetchTime; - if (!settings) { - LOG_ERROR(1000) << "Config JSON is not present. Returning empty map."; +std::unordered_map ConfigCatClient::getAllValues(const std::shared_ptr& user) const { + try { + auto settingResult = getSettings(); + auto& settings = settingResult.settings; + auto& fetchTime = settingResult.fetchTime; + if (!settings) { + LOG_ERROR(1000) << "Config JSON is not present. Returning empty map."; + return {}; + } + + std::unordered_map result; + const auto& effectiveUser = user ? user : defaultUser; + for (const auto& [key, setting] : *settings) { + auto details = evaluate(key, nullopt, effectiveUser, setting, settings, fetchTime); + result.insert({ key, move(details.value) }); + } + + return result; + } + catch (...) { + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1002, std::current_exception()); + logEntry << "Error occurred in the `getAllValues` method. Returning empty map."; return {}; } +} - std::unordered_map result; - for (auto keyValue : *settings) { - auto& key = keyValue.first; - auto details = evaluate(key, user, keyValue.second, fetchTime); - result.insert({key, details.value}); - } +std::vector> ConfigCatClient::getAllValueDetails(const std::shared_ptr& user) const { + try { + auto settingResult = getSettings(); + auto& settings = settingResult.settings; + auto& fetchTime = settingResult.fetchTime; + if (!settings) { + LOG_ERROR(1000) << "Config JSON is not present. Returning empty list."; + return {}; + } - return result; -} + std::vector> result; + const auto& effectiveUser = user ? user : defaultUser; + for (const auto& [key, setting] : *settings) { + result.push_back(evaluate(key, nullopt, effectiveUser, setting, settings, fetchTime)); + } -std::vector ConfigCatClient::getAllValueDetails(const ConfigCatUser* user) const { - auto settingResult = getSettings(); - auto& settings = settingResult.settings; - auto& fetchTime = settingResult.fetchTime; - if (!settings) { - LOG_ERROR(1000) << "Config JSON is not present. Returning empty list."; - return {}; + return result; } - - std::vector result; - for (auto keyValue : *settings) { - auto& key = keyValue.first; - result.push_back(evaluate(key, user, keyValue.second, fetchTime)); + catch (...) { + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1002, std::current_exception()); + logEntry << "Error occurred in the `getAllValueDetails` method. Returning empty list."; + return {}; } - - return result; } template -ValueType ConfigCatClient::_getValue(const std::string& key, const ValueType& defaultValue, const ConfigCatUser* user) const { - auto settingResult = getSettings(); - auto& settings = settingResult.settings; - auto& fetchTime = settingResult.fetchTime; - if (!settings) { - LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1000); - logEntry << "Config JSON is not present when evaluating setting '" << key << "'. Returning the `defaultValue` parameter that you specified in your application: '" << defaultValue << "'."; - hooks->invokeOnFlagEvaluated(EvaluationDetails::fromError(key, defaultValue, logEntry.getMessage())); - return defaultValue; - } +ValueType ConfigCatClient::_getValue(const std::string& key, const ValueType& defaultValue, const std::shared_ptr& user) const { + try { + auto settingResult = getSettings(); + auto& settings = settingResult.settings; + auto& fetchTime = settingResult.fetchTime; + if (!settings) { + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1000); + if constexpr (is_same_v>) { + logEntry << "Config JSON is not present when evaluating setting '" << key << "'. Returning std::nullopt."; + } else { + logEntry << "Config JSON is not present when evaluating setting '" << key << "'. Returning the `defaultValue` parameter that you specified in your application: '" << defaultValue << "'."; + } + hooks->invokeOnFlagEvaluated(EvaluationDetails::fromError(key, defaultValue, logEntry.getMessage())); + return defaultValue; + } - auto setting = settings->find(key); - if (setting == settings->end()) { - vector keys; - keys.reserve(settings->size()); - for (auto keyValue : *settings) { - keys.emplace_back("'" + keyValue.first + "'"); + auto setting = settings->find(key); + if (setting == settings->end()) { + vector keys; + keys.reserve(settings->size()); + for (const auto& [key, _] : *settings) { + keys.emplace_back(key); + } + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1001); + if constexpr (is_same_v>) { + logEntry << + "Failed to evaluate setting '" << key << "' (the key was not found in config JSON). " + "Returning the `defaultValue` parameter that you specified in your application: '" << defaultValue << "'. " + "Available keys: " << keys << "."; + } else { + logEntry << + "Failed to evaluate setting '" << key << "' (the key was not found in config JSON). " + "Returning std::nullopt. Available keys: " << keys << "."; + } + hooks->invokeOnFlagEvaluated(EvaluationDetails::fromError(key, defaultValue, logEntry.getMessage())); + return defaultValue; } - LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1001); - logEntry << - "Failed to evaluate setting '" << key << "' (the key was not found in config JSON). " - "Returning the `defaultValue` parameter that you specified in your application: '" << defaultValue << "'. " - "Available keys: " << keys << "."; - hooks->invokeOnFlagEvaluated(EvaluationDetails::fromError(key, defaultValue, logEntry.getMessage())); - return defaultValue; - } - auto details = evaluate(key, user, setting->second, fetchTime); - const ValueType* valuePtr = get_if(&details.value); - if (valuePtr) - return *valuePtr; + const auto& effectiveUser = user ? user : defaultUser; + auto details = evaluate(key, defaultValue, effectiveUser, setting->second, settings, fetchTime); - LOG_ERROR(1002) << - "Error occurred in the `getValue` method while evaluating setting '" << key << "'. " - "Returning the `defaultValue` parameter that you specified in your application: '" << defaultValue << "'."; - return defaultValue; + return move(details.value); + } + catch (...) { + auto ex = std::current_exception(); + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1002, ex); + logEntry << "Error occurred in the `getValue` method while evaluating setting '" << key << "'. "; + if constexpr (is_same_v>) { + logEntry << "Returning std::nullopt."; + } else { + logEntry << "Returning the `defaultValue` parameter that you specified in your application: '" << defaultValue << "'."; + } + auto details = EvaluationDetails::fromError(key, defaultValue, logEntry.getMessage(), ex); + hooks->invokeOnFlagEvaluated(details); + return defaultValue; + } } -EvaluationDetails ConfigCatClient::evaluate(const std::string& key, - const ConfigCatUser* user, - const Setting& setting, - double fetchTime) const { - user = user != nullptr ? user : defaultUser.get(); - auto [value, variationId, rule, percentageRule, error] = rolloutEvaluator->evaluate(key, user, setting); - - EvaluationDetails details(key, - value, - variationId, - time_point>(duration(fetchTime)), - user, - error.empty() ? false : true, - error, - rule, - percentageRule); +template +EvaluationDetails ConfigCatClient::evaluate(const std::string& key, + const std::optional& defaultValue, + const std::shared_ptr& effectiveUser, + const Setting& setting, + const std::shared_ptr& settings, + double fetchTime) const { + EvaluateContext evaluateContext(key, setting, effectiveUser, settings); + std::optional returnValue; + auto evaluateResult = rolloutEvaluator->evaluate(defaultValue, evaluateContext, returnValue); + + ValueType value; + if constexpr (is_same_v || is_same_v || is_same_v || is_same_v) { + // RolloutEvaluator::evaluate makes sure that this variant access is always valid. + value = std::get(*returnValue); + } else if constexpr (is_same_v) { + value = *returnValue; + } else if constexpr (is_same_v>) { + value = returnValue; + } else { + static_assert(always_false_v, "Unsupported value type."); + } + + EvaluationDetails details(key, + value, + evaluateResult.selectedValue.variationId, + time_point>(duration(fetchTime)), + effectiveUser, + false, + nullopt, + nullptr, + evaluateResult.targetingRule, + evaluateResult.percentageOption); hooks->invokeOnFlagEvaluated(details); return details; } -void ConfigCatClient::forceRefresh() { - if (configService) { - configService->refresh(); +RefreshResult ConfigCatClient::forceRefresh() { + try { + return configService + ? configService->refresh() + : RefreshResult{"Client is configured to use the LocalOnly override behavior or has been closed, which prevents making HTTP requests."}; + } catch (...) { + auto ex = std::current_exception(); + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1003, ex); + logEntry << "Error occurred in the `forceRefresh` method."; + return RefreshResult{logEntry.getMessage(), ex}; } } void ConfigCatClient::setOnline() { if (configService) { configService->setOnline(); - } - else { - LOG_WARN(3202) << "Client is configured to use the `LocalOnly` override behavior, thus `setOnline()` has no effect."; + } else { + LOG_WARN(3202) << "Client is configured to use the `LocalOnly` override behavior or has been closed, thus `setOnline()` has no effect."; } } diff --git a/include/configcat/configcatlogger.h b/src/configcatlogger.h similarity index 55% rename from include/configcat/configcatlogger.h rename to src/configcatlogger.h index daeb89e..f3dcafd 100644 --- a/include/configcat/configcatlogger.h +++ b/src/configcatlogger.h @@ -1,31 +1,33 @@ #pragma once -#include "log.h" -#include "configcatoptions.h" -#include "configcatuser.h" -#include "config.h" +#include + +#include "configcat/log.h" +#include "configcat/configcatoptions.h" +#include "configcat/configcatuser.h" +#include "configcat/config.h" +#include "utils.h" namespace configcat { class ConfigCatLogger { public: - ConfigCatLogger(std::shared_ptr logger, std::shared_ptr hooks): + ConfigCatLogger(const std::shared_ptr& logger, const std::shared_ptr& hooks): logger(logger), hooks(hooks) { } - void log(LogLevel level, int eventId, const std::string& message) { + void log(LogLevel level, int eventId, const std::string& message, const std::exception_ptr& exception = nullptr) { if (hooks && level == LOG_LEVEL_ERROR) { - hooks->invokeOnError(message); + hooks->invokeOnError(message, exception); } - if (logger) { - logger->log(level, "[" + std::to_string(eventId) + "] " + message); + if (isEnabled(level)) { + logger->log(level, "[" + std::to_string(eventId) + "] " + message, exception); } } - void setLogLevel(LogLevel logLevel) { if (logger) logger->setLogLevel(logLevel); } - LogLevel getLogLevel() const { return logger ? logger->getLogLevel() : LOG_LEVEL_WARNING; } + inline bool isEnabled(LogLevel level) { return logger && level <= logger->getLogLevel(); } private: std::shared_ptr logger; @@ -34,70 +36,55 @@ class ConfigCatLogger { class LogEntry { public: - LogEntry(std::shared_ptr logger, LogLevel level, int eventId) : logger(logger), level(level), eventId(eventId) {} + LogEntry(const std::shared_ptr& logger, LogLevel level, int eventId, const std::exception_ptr& exception = nullptr) + : logger(logger), level(level), eventId(eventId), exception(exception) {} + ~LogEntry() { - if (logger && level <= logger->getLogLevel()) - logger->log(level, eventId, message); + if (logger->isEnabled(level)) + logger->log(level, eventId, message, exception); } LogEntry& operator<<(const char* str) { - if (str && logger && level <= logger->getLogLevel()) + if (str && logger->isEnabled(level)) message += str; return *this; } LogEntry& operator<<(char* str) { - if (str && logger && level <= logger->getLogLevel()) + if (str && logger->isEnabled(level)) message += str; return *this; } LogEntry& operator<<(const std::string& str) { - if (logger && level <= logger->getLogLevel()) + if (logger->isEnabled(level)) message += str; return *this; } LogEntry& operator<<(bool arg) { - if (logger && level <= logger->getLogLevel()) + if (logger->isEnabled(level)) message += arg ? "true" : "false"; return *this; } - LogEntry& operator<<(const ConfigCatUser* user) { - return operator<<(*user); - } - - LogEntry& operator<<(const ConfigCatUser& user) { - if (logger && level <= logger->getLogLevel()) - message += user.toJson(); - return *this; - } - - LogEntry& operator<<(const Value& v) { - if (logger && level <= logger->getLogLevel()) - message += valueToString(v); + LogEntry& operator<<(const std::optional& v) { + if (logger->isEnabled(level)) + message += v ? v->toString() : ""; return *this; } template LogEntry& operator<<(Type arg) { - if (logger && level <= logger->getLogLevel()) + if (logger->isEnabled(level)) message += std::to_string(arg); return *this; } - template - LogEntry& operator<<(const std::vector& v) { - if (logger && level <= logger->getLogLevel()) { + LogEntry& operator<<(const std::vector& v) { + if (logger->isEnabled(level)) { message += "["; - size_t last = v.size() - 1; - for (size_t i = 0; i < v.size(); ++i) { - operator<<(v[i]); - if (i != last) { - message += ", "; - } - } + append_stringlist(*this, v); message += "]"; } return *this; @@ -110,6 +97,7 @@ class LogEntry { LogLevel level; int eventId; std::string message; + std::exception_ptr exception; }; } // namespace configcat diff --git a/src/configcatuser.cpp b/src/configcatuser.cpp index 2030f5f..e96b810 100644 --- a/src/configcatuser.cpp +++ b/src/configcatuser.cpp @@ -1,34 +1,50 @@ #include "configcat/configcatuser.h" #include + using namespace std; -using json = nlohmann::json; +using ordered_json = nlohmann::ordered_json; namespace configcat { -ConfigCatUser::ConfigCatUser(const string& id, - const string& email, - const string& country, - const unordered_map& custom): - identifier(attributes["Identifier"]) { - attributes["Identifier"] = id; - if (!email.empty()) attributes["Email"] = email; - if (!country.empty()) attributes["Country"] = country; - attributes.insert(custom.begin(), custom.end()); -} - -const string* ConfigCatUser::getAttribute(const string& key) const { - auto it = attributes.find(key); - if (it != attributes.end()) { +const ConfigCatUser::AttributeValue* ConfigCatUser::getAttribute(const string& key) const { + if (key == ConfigCatUser::kIdentifierAttribute) { + return &identifier; + } + if (key == ConfigCatUser::kEmailAttribute) { + return email ? &*email : nullptr; + } + if (key == ConfigCatUser::kCountryAttribute) { + return country ? &*country : nullptr; + } + if (auto it = custom.find(key); it != custom.end()) { return &it->second; } - return nullptr; } string ConfigCatUser::toJson() const { - json user(attributes); - return user.dump(4); + ordered_json j = { + { kIdentifierAttribute, get(identifier) } + }; + + if (email) j[kEmailAttribute] = get(*email); + if (country) j[kCountryAttribute] = get(*country); + + for (const auto& [name, setting] : custom) { + if (name != kIdentifierAttribute && name != kEmailAttribute && name != kCountryAttribute) { + visit([&j, &nameRef = name] (auto&& alt) { // rebind reference to keep clang compiler happy (https://stackoverflow.com/a/74376436) + using T = decay_t; + if constexpr (is_same_v) { + j[nameRef] = datetime_to_isostring(alt); + } else { + j[nameRef] = alt; + } + }, setting); + } + } + + return j.dump(); } } // namespace configcat diff --git a/src/configentry.cpp b/src/configentry.cpp new file mode 100644 index 0000000..5484dd1 --- /dev/null +++ b/src/configentry.cpp @@ -0,0 +1,49 @@ +#include +#include + +#include "configcat/config.h" +#include "configcat/log.h" +#include "configentry.h" + +using namespace std; + +namespace configcat { + +const shared_ptr ConfigEntry::empty = make_shared(Config::empty, "empty"); + +shared_ptr ConfigEntry::fromString(const string& text) { + if (text.empty()) + return ConfigEntry::empty; + + auto fetchTimeIndex = text.find('\n'); + auto eTagIndex = text.find('\n', fetchTimeIndex + 1); + if (fetchTimeIndex == string::npos || eTagIndex == string::npos) { + throw invalid_argument("Number of values is fewer than expected."); + } + + auto fetchTimeString = text.substr(0, fetchTimeIndex); + double fetchTime; + try { + fetchTime = stod(fetchTimeString); + } catch (...) { + throw invalid_argument("Invalid fetch time: " + fetchTimeString + ". " + unwrap_exception_message(current_exception())); + } + + auto eTag = text.substr(fetchTimeIndex + 1, eTagIndex - fetchTimeIndex - 1); + if (eTag.empty()) { + throw invalid_argument("Empty eTag value"); + } + + auto configJsonString = text.substr(eTagIndex + 1); + try { + return make_shared(Config::fromJson(configJsonString), eTag, configJsonString, fetchTime / 1000.0); + } catch (...) { + throw invalid_argument("Invalid config JSON: " + configJsonString + ". " + unwrap_exception_message(current_exception())); + } +} + +string ConfigEntry::serialize() const { + return to_string(static_cast(floor(fetchTime * 1000))) + "\n" + eTag + "\n" + configJsonString; +} + +} // namespace configcat diff --git a/src/configentry.h b/src/configentry.h new file mode 100644 index 0000000..9d8d9be --- /dev/null +++ b/src/configentry.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include "configcat/config.h" + +namespace configcat { + +// extra brackets to avoid numeric_limits::max()/min() not recognized error on windows +constexpr double kDistantFuture = (std::numeric_limits::max)(); +constexpr double kDistantPast = (std::numeric_limits::min)(); + +struct ConfigEntry { + static constexpr char kConfig[] = "config"; + static constexpr char kETag[] = "etag"; + static constexpr char kFetchTime[] = "fetch_time"; + static constexpr char kSerializationFormatVersion[] = "v2"; + + static const std::shared_ptr empty; + + ConfigEntry(const std::shared_ptr& config = Config::empty, + const std::string& eTag = "", + const std::string& configJsonString = "{}", + double fetchTime = kDistantPast): + config(config), + eTag(eTag), + configJsonString(configJsonString), + fetchTime(fetchTime) { + } + ConfigEntry(const ConfigEntry&) = delete; // Disable copy + + static std::shared_ptr fromString(const std::string& text); + std::string serialize() const; + + std::shared_ptr config; + std::string eTag; + std::string configJsonString; + double fetchTime; +}; + +} // namespace configcat diff --git a/src/configfetcher.cpp b/src/configfetcher.cpp index 40776b6..2526424 100644 --- a/src/configfetcher.cpp +++ b/src/configfetcher.cpp @@ -3,17 +3,17 @@ #include "configfetcher.h" #include "configcat/log.h" #include "configcat/configcatoptions.h" -#include "configcat/configcatlogger.h" +#include "configcat/timeutils.h" +#include "configcatlogger.h" #include "curlnetworkadapter.h" #include "version.h" #include "platform.h" -#include "utils.h" using namespace std; namespace configcat { -ConfigFetcher::ConfigFetcher(const string& sdkKey, shared_ptr logger, const string& mode, const ConfigCatOptions& options): +ConfigFetcher::ConfigFetcher(const string& sdkKey, const shared_ptr& logger, const string& mode, const ConfigCatOptions& options): sdkKey(sdkKey), logger(logger), mode(mode), @@ -57,34 +57,35 @@ FetchResponse ConfigFetcher::fetchConfiguration(const std::string& eTag) { FetchResponse ConfigFetcher::executeFetch(const std::string& eTag, int executeCount) { auto response = fetch(eTag); - auto preferences = response.entry && response.entry->config ? response.entry->config->preferences : nullptr; + auto& preferences = response.entry && response.entry->config ? response.entry->config->preferences : nullopt; // If there wasn't a config change or there were no preferences in the config, we return the response - if (!response.isFetched() || preferences == nullptr) { + if (!response.isFetched() || !preferences) { return response; } + const auto& baseUrl = preferences->baseUrl.value_or(""); // If the preferences url is the same as the last called one, just return the response. - if (!preferences->url.empty() && url == preferences->url) { + if (!baseUrl.empty() && url == baseUrl) { return response; } // If the url is overridden, and the redirect parameter is not ForceRedirect, // the SDK should not redirect the calls, and it just has to return the response. - if (urlIsCustom && preferences->redirect != ForceRedirect) { + if (urlIsCustom && preferences->redirectMode != RedirectMode::Force) { return response; } // The next call should use the preferences url provided in the config json - url = preferences->url; + url = baseUrl; - if (preferences->redirect == NoRedirect) { + if (preferences->redirectMode == RedirectMode::No) { return response; } // Try to download again with the new url - if (preferences->redirect == ShouldRedirect) { + if (preferences->redirectMode == RedirectMode::Should) { LOG_WARN(3002) << "The `dataGovernance` parameter specified at the client initialization is not in sync with the preferences on the ConfigCat Dashboard. " "Read more: https://configcat.com/docs/advanced/data-governance/"; @@ -103,7 +104,7 @@ FetchResponse ConfigFetcher::fetch(const std::string& eTag) { auto error = "HttpSessionAdapter is not provided."; LOG_ERROR(0) << error; assert(false); - return FetchResponse(failure, ConfigEntry::empty, error, true); + return FetchResponse(failure, ConfigEntry::empty, error, nullptr, true); } string requestUrl(url + "/configuration-files/" + sdkKey + "/" + kConfigJsonName); @@ -116,16 +117,29 @@ FetchResponse ConfigFetcher::fetch(const std::string& eTag) { } auto response = httpSessionAdapter->get(requestUrl, requestHeader, proxies, proxyAuthentications); - if (response.operationTimedOut) { + if (response.errorCode == ResponseErrorCode::TimedOut) { LogEntry logEntry = LogEntry(logger, LOG_LEVEL_ERROR, 1102); logEntry << "Request timed out while trying to fetch config JSON. " "Timeout values: [connect: " << connectTimeoutMs << "ms, read: " << readTimeoutMs << "ms]"; - return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), true); + return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), nullptr, true); + } + if (response.errorCode == ResponseErrorCode::RequestCancelled) { + auto message = "Request was cancelled while trying to fetch config JSON."; + LOG_INFO(0) << message; + return FetchResponse(failure, ConfigEntry::empty, message, nullptr, true); } if (response.error.length() > 0) { - LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1103); - logEntry << "Unexpected error occurred while trying to fetch config JSON: " << response.error; - return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), true); + try { throw std::runtime_error(response.error); } + catch (...) + { + exception_ptr ex = current_exception(); + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1103, ex); + logEntry << + "Unexpected error occurred while trying to fetch config JSON. " + "It is most likely due to a local network issue. " + "Please make sure your application can reach the ConfigCat CDN servers (or your proxy server) over HTTP."; + return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), ex, true); + } } switch (response.statusCode) { @@ -134,18 +148,23 @@ FetchResponse ConfigFetcher::fetch(const std::string& eTag) { case 202: case 203: case 204: { - const auto it = response.header.find(kEtagHeaderName); + std::map::const_iterator it = response.header.find(kEtagHeaderName); + // If the etag header is not present in the response, try to find it case-insensitively + if (it == response.header.end()) { + it = find_caseinsensitive(response.header, kEtagHeaderName); + } string eTag = it != response.header.end() ? it->second : ""; try { auto config = Config::fromJson(response.text); LOG_DEBUG << "Fetch was successful: new config fetched."; - return FetchResponse(fetched, make_shared(config, eTag, response.text, getUtcNowSecondsSinceEpoch())); - } catch (exception& exception) { - LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1105); + return FetchResponse(fetched, make_shared(config, eTag, response.text, get_utcnowseconds_since_epoch())); + } catch (...) { + auto ex = current_exception(); + LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1105, ex); logEntry << "Fetching config JSON was successful but the HTTP response content was invalid. " - "Config JSON parsing failed. " << exception.what(); - return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), true); + "Config JSON parsing failed."; + return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), ex, true); } } @@ -159,13 +178,13 @@ FetchResponse ConfigFetcher::fetch(const std::string& eTag) { logEntry << "Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey. " "Received unexpected response: " << response.statusCode; - return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), false); + return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), nullptr, false); } default: { LogEntry logEntry(logger, LOG_LEVEL_ERROR, 1101); logEntry << "Unexpected HTTP response was received while trying to fetch config JSON: " << response.statusCode; - return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), true); + return FetchResponse(failure, ConfigEntry::empty, logEntry.getMessage(), nullptr, true); } } } diff --git a/src/configfetcher.h b/src/configfetcher.h index f9f0e6a..2aa0bb7 100644 --- a/src/configfetcher.h +++ b/src/configfetcher.h @@ -6,12 +6,12 @@ #include #include "configcat/proxyauthentication.h" +#include "configentry.h" namespace configcat { struct ConfigCatOptions; class ConfigCatLogger; -struct ConfigEntry; class HttpSessionAdapter; enum Status { @@ -22,15 +22,18 @@ enum Status { struct FetchResponse { Status status; - std::shared_ptr entry; - std::string error; + std::shared_ptr entry; + std::optional errorMessage; + std::exception_ptr errorException; bool isTransientError = false; - FetchResponse(Status status, std::shared_ptr entry, const std::string& error = "", bool isTransientError = false): - status(status), - entry(entry), - error(error), - isTransientError(isTransientError) { + FetchResponse(Status status, const std::shared_ptr& entry, + const std::optional& errorMessage = std::nullopt, const std::exception_ptr& errorException = nullptr, bool isTransientError = false) + : status(status) + , entry(entry) + , errorMessage(errorMessage) + , errorException(errorException) + , isTransientError(isTransientError) { } bool isFetched() { @@ -48,15 +51,15 @@ struct FetchResponse { class ConfigFetcher { public: - static constexpr char kConfigJsonName[] = "config_v5.json"; + static constexpr char kConfigJsonName[] = "config_v6.json"; static constexpr char kGlobalBaseUrl[] = "https://cdn-global.configcat.com"; static constexpr char kEuOnlyBaseUrl[] = "https://cdn-eu.configcat.com"; static constexpr char kUserAgentHeaderName[] = "X-ConfigCat-UserAgent"; static constexpr char kPlatformHeaderName[] = "X-ConfigCat-Platform"; static constexpr char kIfNoneMatchHeaderName[] = "If-None-Match"; - static constexpr char kEtagHeaderName[] = "ETag"; + static constexpr char kEtagHeaderName[] = "etag"; - ConfigFetcher(const std::string& sdkKey, std::shared_ptr logger, const std::string& mode, const ConfigCatOptions& options); + ConfigFetcher(const std::string& sdkKey, const std::shared_ptr& logger, const std::string& mode, const ConfigCatOptions& options); ~ConfigFetcher(); void close(); diff --git a/src/configservice.cpp b/src/configservice.cpp index 60c2898..cdb9357 100644 --- a/src/configservice.cpp +++ b/src/configservice.cpp @@ -1,8 +1,8 @@ #include "configservice.h" #include "configcat/configcatoptions.h" -#include "configcat/configcatlogger.h" +#include "configcat/timeutils.h" +#include "configcatlogger.h" #include "configfetcher.h" -#include "utils.h" #include #include @@ -12,14 +12,14 @@ using namespace std::this_thread; namespace configcat { ConfigService::ConfigService(const string& sdkKey, - shared_ptr logger, - std::shared_ptr hooks, - std::shared_ptr configCache, + const shared_ptr& logger, + const std::shared_ptr& hooks, + const std::shared_ptr& configCache, const ConfigCatOptions& options): logger(logger), hooks(hooks), pollingMode(options.pollingMode ? options.pollingMode : PollingMode::autoPoll()), - cachedEntry(ConfigEntry::empty), + cachedEntry(const_pointer_cast(ConfigEntry::empty)), configCache(configCache) { cacheKey = generateCacheKey(sdkKey); configFetcher = make_unique(sdkKey, logger, pollingMode->getPollingIdentifier(), options); @@ -48,9 +48,9 @@ SettingResult ConfigService::getSettings() { if (pollingMode->getPollingIdentifier() == LazyLoadingMode::kIdentifier) { auto& lazyPollingMode = (LazyLoadingMode&)*pollingMode; auto now = chrono::steady_clock::now(); - auto [ entry, _ ] = fetchIfOlder(getUtcNowSecondsSinceEpoch() - lazyPollingMode.cacheRefreshIntervalInSeconds); + auto [ entry, _0, _1 ] = fetchIfOlder(get_utcnowseconds_since_epoch() - lazyPollingMode.cacheRefreshIntervalInSeconds); auto config = cachedEntry->config; - return { (cachedEntry != ConfigEntry::empty && config) ? config->entries : nullptr, entry->fetchTime }; + return { (cachedEntry != ConfigEntry::empty && config) ? config->getSettingsOrEmpty() : nullptr, entry->fetchTime}; } else if (pollingMode->getPollingIdentifier() == AutoPollingMode::kIdentifier && !initialized) { auto& autoPollingMode = (AutoPollingMode&)*pollingMode; auto elapsedTime = chrono::duration(chrono::steady_clock::now() - startTime).count(); @@ -63,19 +63,26 @@ SettingResult ConfigService::getSettings() { if (!initialized) { setInitialized(); auto config = cachedEntry->config; - return { (cachedEntry != ConfigEntry::empty && config) ? config->entries : nullptr, cachedEntry->fetchTime }; + return { (cachedEntry != ConfigEntry::empty && config) ? config->getSettingsOrEmpty() : nullptr, cachedEntry->fetchTime }; } } } - auto [ entry, _ ] = fetchIfOlder(kDistantPast, true); + // If we are initialized, we prefer the cached results + auto [ entry, _0, _1 ] = fetchIfOlder(kDistantPast, initialized); auto config = entry->config; - return { (cachedEntry != ConfigEntry::empty && config) ? config->entries : nullptr, entry->fetchTime }; + return { (cachedEntry != ConfigEntry::empty && config) ? config->getSettingsOrEmpty() : nullptr, entry->fetchTime }; } RefreshResult ConfigService::refresh() { - auto [ _, error ] = fetchIfOlder(kDistantFuture); - return { error.empty(), error }; + if (offline) { + auto offlineWarning = "Client is in offline mode, it cannot initiate HTTP calls."; + LOG_WARN(3200) << offlineWarning; + return { offlineWarning, nullptr }; + } + + auto [ _, errorMessage, errorException ] = fetchIfOlder(kDistantFuture); + return { errorMessage, errorException }; } void ConfigService::setOnline() { @@ -113,37 +120,26 @@ void ConfigService::setOffline() { string ConfigService::generateCacheKey(const string& sdkKey) { return SHA1()(sdkKey + "_" + ConfigFetcher::kConfigJsonName + "_" + ConfigEntry::kSerializationFormatVersion); } -tuple, string> ConfigService::fetchIfOlder(double time, bool preferCache) { +tuple, std::optional, std::exception_ptr> ConfigService::fetchIfOlder(double threshold, bool preferCache) { { lock_guard lock(fetchMutex); // Sync up with the cache and use it when it's not expired. - if (cachedEntry == ConfigEntry::empty || cachedEntry->fetchTime > time) { - auto entry = readCache(); - if (entry != ConfigEntry::empty && entry->eTag != cachedEntry->eTag) { - cachedEntry = entry; - hooks->invokeOnConfigChanged(entry->config->entries); - } - - // Cache isn't expired - if (cachedEntry && cachedEntry->fetchTime > time) { - setInitialized(); - return { cachedEntry, "" }; - } + auto fromCache = readCache(); + if (fromCache != ConfigEntry::empty && fromCache->eTag != cachedEntry->eTag) { + cachedEntry = const_pointer_cast(fromCache); + hooks->invokeOnConfigChanged(fromCache->config->getSettingsOrEmpty()); } - // Use cache anyway (get calls on auto & manual poll must not initiate fetch). - // The initialized check ensures that we subscribe for the ongoing fetch during the - // max init wait time window in case of auto poll. - if (preferCache && initialized) { - return { cachedEntry, "" }; + // Cache isn't expired + if (cachedEntry && cachedEntry->fetchTime > threshold) { + setInitialized(); + return { cachedEntry, nullopt, nullptr }; } - // If we are in offline mode we are not allowed to initiate fetch. - if (offline) { - auto offlineWarning = "Client is in offline mode, it cannot initiate HTTP calls."; - LOG_WARN(3200) << offlineWarning; - return { cachedEntry, offlineWarning }; + // If we are in offline mode or the caller prefers cached values, do not initiate fetch. + if (offline || preferCache) { + return { cachedEntry, nullopt, nullptr }; } // If there's an ongoing fetch running, we will wait for the ongoing fetch future and use its response. @@ -164,16 +160,16 @@ tuple, string> ConfigService::fetchIfOlder(double time, lock_guard lock(fetchMutex); if (response.isFetched()) { - cachedEntry = response.entry; + cachedEntry = const_pointer_cast(response.entry); writeCache(cachedEntry); - hooks->invokeOnConfigChanged(cachedEntry->config->entries); + hooks->invokeOnConfigChanged(cachedEntry->config->getSettingsOrEmpty()); } else if ((response.notModified() || !response.isTransientError) && cachedEntry != ConfigEntry::empty) { - cachedEntry->fetchTime = getUtcNowSecondsSinceEpoch(); + cachedEntry->fetchTime = get_utcnowseconds_since_epoch(); writeCache(cachedEntry); } setInitialized(); - return { cachedEntry, "" }; + return { cachedEntry, response.errorMessage, response.errorException }; } void ConfigService::setInitialized() { @@ -184,7 +180,7 @@ void ConfigService::setInitialized() { } } -shared_ptr ConfigService::readCache() { +shared_ptr ConfigService::readCache() { try { auto jsonString = configCache->read(cacheKey); if (jsonString.empty() || jsonString == cachedEntryString) { @@ -193,17 +189,19 @@ shared_ptr ConfigService::readCache() { cachedEntryString = jsonString; return ConfigEntry::fromString(jsonString); - } catch (exception& exception) { - LOG_ERROR(2200) << "Error occurred while reading the cache. " << exception.what(); + } catch (...) { + LogEntry logEntry(logger, configcat::LOG_LEVEL_ERROR, 2200, current_exception()); + logEntry << "Error occurred while reading the cache."; return ConfigEntry::empty; } } -void ConfigService::writeCache(const std::shared_ptr& configEntry) { +void ConfigService::writeCache(const std::shared_ptr& configEntry) { try { configCache->write(cacheKey, configEntry->serialize()); - } catch (exception& exception) { - LOG_ERROR(2201) << "Error occurred while writing the cache. " << exception.what(); + } catch (...) { + LogEntry logEntry(logger, configcat::LOG_LEVEL_ERROR, 2201, current_exception()); + logEntry << "Error occurred while writing the cache."; } } @@ -213,7 +211,7 @@ void ConfigService::startPoll() { void ConfigService::run() { auto& autoPollingMode = (AutoPollingMode&)*pollingMode; - fetchIfOlder(getUtcNowSecondsSinceEpoch() - autoPollingMode.autoPollIntervalInSeconds); + fetchIfOlder(get_utcnowseconds_since_epoch() - autoPollingMode.autoPollIntervalInSeconds); { // Initialization finished @@ -232,7 +230,7 @@ void ConfigService::run() { } } - fetchIfOlder(getUtcNowSecondsSinceEpoch() - autoPollingMode.autoPollIntervalInSeconds); + fetchIfOlder(get_utcnowseconds_since_epoch() - autoPollingMode.autoPollIntervalInSeconds); } while (true); } diff --git a/src/configservice.h b/src/configservice.h index 1992c77..af2013d 100644 --- a/src/configservice.h +++ b/src/configservice.h @@ -8,7 +8,7 @@ #include #include "configcat/config.h" #include "configcat/refreshresult.h" -#include "configcat/settingresult.h" +#include "settingresult.h" #include "configfetcher.h" @@ -18,16 +18,15 @@ struct ConfigCatOptions; class ConfigCatLogger; class ConfigFetcher; class ConfigCache; -struct ConfigEntry; class PollingMode; class Hooks; class ConfigService { public: ConfigService(const std::string& sdkKey, - std::shared_ptr logger, - std::shared_ptr hooks, - std::shared_ptr configCache, + const std::shared_ptr& logger, + const std::shared_ptr& hooks, + const std::shared_ptr& configCache, const ConfigCatOptions& options); ~ConfigService(); @@ -41,10 +40,10 @@ class ConfigService { private: // Returns the ConfigEntry object and error message in case of any error. - std::tuple, std::string> fetchIfOlder(double time, bool preferCache = false); + std::tuple, std::optional, std::exception_ptr> fetchIfOlder(double threshold, bool preferCache = false); void setInitialized(); - std::shared_ptr readCache(); - void writeCache(const std::shared_ptr& configEntry); + std::shared_ptr readCache(); + void writeCache(const std::shared_ptr& configEntry); void startPoll(); void run(); diff --git a/src/curlnetworkadapter.cpp b/src/curlnetworkadapter.cpp index 69e7409..aff782d 100644 --- a/src/curlnetworkadapter.cpp +++ b/src/curlnetworkadapter.cpp @@ -41,7 +41,9 @@ std::shared_ptr LibCurlResourceGuard::instance = nullptr; std::mutex LibCurlResourceGuard::mutex; int ProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { - return static_cast(clientp)->ProgressFunction(dltotal, dlnow, ultotal, ulnow); + if (clientp) + return static_cast(clientp)->ProgressFunction(dltotal, dlnow, ultotal, ulnow); + return 1; // Return abort } int CurlNetworkAdapter::ProgressFunction(curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { @@ -103,6 +105,7 @@ Response CurlNetworkAdapter::get(const std::string& url, const std::map& proxyAuthentications) { Response response; if (!curl) { + response.errorCode = ResponseErrorCode::InternalError; response.error = "CURL is not initialized."; return response; } @@ -139,7 +142,13 @@ Response CurlNetworkAdapter::get(const std::string& url, if (res != CURLE_OK) { response.error = curl_easy_strerror(res); - response.operationTimedOut = res == CURLE_OPERATION_TIMEDOUT; + if (res == CURLE_OPERATION_TIMEDOUT) { + response.errorCode = ResponseErrorCode::TimedOut; + } else if (res == CURLE_ABORTED_BY_CALLBACK) { + response.errorCode = ResponseErrorCode::RequestCancelled; + } else { + response.errorCode = ResponseErrorCode::InternalError; + } return response; } @@ -157,6 +166,7 @@ void CurlNetworkAdapter::close() { CurlNetworkAdapter::~CurlNetworkAdapter() { if (curl) { + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, NULL); curl_easy_cleanup(curl); } } diff --git a/src/evaluatelogbuilder.cpp b/src/evaluatelogbuilder.cpp new file mode 100644 index 0000000..f434c81 --- /dev/null +++ b/src/evaluatelogbuilder.cpp @@ -0,0 +1,349 @@ +#include + +#include "evaluatelogbuilder.h" +#include "configcat/timeutils.h" + +using namespace std; + +namespace configcat { + +static constexpr char kInvalidNamePlaceholder[] = ""; +static constexpr char kInvalidOperatorPlaceholder[] = ""; +static constexpr char kInvalidReferencePlaceholder[] = ""; +static constexpr char kInvalidValuePlaceholder[] = ""; + +static constexpr char kValueText[] = "value"; +static constexpr char kValuesText[] = "values"; + +static constexpr size_t kStringListMaxCount = 10; + +EvaluateLogBuilder& configcat::EvaluateLogBuilder::newLine() { + // TODO: platform-specific line terminator? + ss << endl; + for (auto i = 0; i < this->indentLevel; i++) { + ss << " "; + } + return *this; +} + +EvaluateLogBuilder& EvaluateLogBuilder::appendUserConditionCore(const std::string& comparisonAttribute, UserComparator comparator, const std::optional& comparisonValue) { + return appendFormat("User.%s %s '%s'", + comparisonAttribute.c_str(), + getUserComparatorText(comparator), + comparisonValue ? comparisonValue->c_str() : kInvalidValuePlaceholder); +} + +EvaluateLogBuilder& EvaluateLogBuilder::appendUserConditionString(const std::string& comparisonAttribute, UserComparator comparator, const UserConditionComparisonValue& comparisonValue, bool isSensitive) { + const auto comparisonValuePtr = get_if(&comparisonValue); + if (!comparisonValuePtr) { + return appendUserConditionCore(comparisonAttribute, comparator, nullopt); + } + + return appendUserConditionCore(comparisonAttribute, comparator, isSensitive ? "" : *comparisonValuePtr); +} + +EvaluateLogBuilder& EvaluateLogBuilder::appendUserConditionStringList(const std::string& comparisonAttribute, UserComparator comparator, const UserConditionComparisonValue& comparisonValue, bool isSensitive) { + const auto comparisonValuesPtr = get_if>(&comparisonValue); + if (!comparisonValuesPtr) { + return appendUserConditionCore(comparisonAttribute, comparator, nullopt); + } + + if (isSensitive) { + const auto comparisonValueCount = comparisonValuesPtr->size(); + return appendFormat("User.%s %s [<%d hashed %s>]", + comparisonAttribute.c_str(), + getUserComparatorText(comparator), + comparisonValueCount, + comparisonValueCount == 1 ? kValueText : kValuesText); + } + else { + appendFormat("User.%s %s [", + comparisonAttribute.c_str(), + getUserComparatorText(comparator)); + + append_stringlist(this->ss, *comparisonValuesPtr, kStringListMaxCount, [](size_t count) { + return string_format(", ... <%d more %s>", count, count == 1 ? kValueText : kValuesText); + }); + + return append("]"); + } +} + +EvaluateLogBuilder& EvaluateLogBuilder::appendUserConditionNumber(const std::string& comparisonAttribute, UserComparator comparator, const UserConditionComparisonValue& comparisonValue, bool isDateTime = false) { + const auto comparisonValuePtr = get_if(&comparisonValue); + if (!comparisonValuePtr) { + return appendUserConditionCore(comparisonAttribute, comparator, nullopt); + } + + if (isDateTime) { + if (const auto tp = datetime_from_unixtimeseconds(*comparisonValuePtr); tp) { + return appendFormat("User.%s %s '%s' (%s UTC)", + comparisonAttribute.c_str(), + getUserComparatorText(comparator), + number_to_string(*comparisonValuePtr).c_str(), + datetime_to_isostring(*tp).c_str()); + } + } + + return appendFormat("User.%s %s '%g'", + comparisonAttribute.c_str(), + getUserComparatorText(comparator), + *comparisonValuePtr); +} + +EvaluateLogBuilder& EvaluateLogBuilder::appendUserCondition(const UserCondition& condition) { + const auto& comparisonAttribute = condition.comparisonAttribute; + const auto comparator = condition.comparator; + const auto& comparisonValue = condition.comparisonValue; + + switch (comparator) { + case UserComparator::TextIsOneOf: + case UserComparator::TextIsNotOneOf: + case UserComparator::TextContainsAnyOf: + case UserComparator::TextNotContainsAnyOf: + case UserComparator::SemVerIsOneOf: + case UserComparator::SemVerIsNotOneOf: + case UserComparator::TextStartsWithAnyOf: + case UserComparator::TextNotStartsWithAnyOf: + case UserComparator::TextEndsWithAnyOf: + case UserComparator::TextNotEndsWithAnyOf: + case UserComparator::ArrayContainsAnyOf: + case UserComparator::ArrayNotContainsAnyOf: + return appendUserConditionStringList(comparisonAttribute, comparator, comparisonValue, false); + + case UserComparator::SemVerLess: + case UserComparator::SemVerLessOrEquals: + case UserComparator::SemVerGreater: + case UserComparator::SemVerGreaterOrEquals: + case UserComparator::TextEquals: + case UserComparator::TextNotEquals: + return appendUserConditionString(comparisonAttribute, comparator, comparisonValue, false); + + case UserComparator::NumberEquals: + case UserComparator::NumberNotEquals: + case UserComparator::NumberLess: + case UserComparator::NumberLessOrEquals: + case UserComparator::NumberGreater: + case UserComparator::NumberGreaterOrEquals: + return appendUserConditionNumber(comparisonAttribute, comparator, comparisonValue); + + case UserComparator::SensitiveTextIsOneOf: + case UserComparator::SensitiveTextIsNotOneOf: + case UserComparator::SensitiveTextStartsWithAnyOf: + case UserComparator::SensitiveTextNotStartsWithAnyOf: + case UserComparator::SensitiveTextEndsWithAnyOf: + case UserComparator::SensitiveTextNotEndsWithAnyOf: + case UserComparator::SensitiveArrayContainsAnyOf: + case UserComparator::SensitiveArrayNotContainsAnyOf: + return appendUserConditionStringList(comparisonAttribute, comparator, comparisonValue, true); + + case UserComparator::DateTimeBefore: + case UserComparator::DateTimeAfter: + return appendUserConditionNumber(comparisonAttribute, comparator, comparisonValue, true); + + case UserComparator::SensitiveTextEquals: + case UserComparator::SensitiveTextNotEquals: + return appendUserConditionString(comparisonAttribute, comparator, comparisonValue, true); + + default: { + string str; + return appendUserConditionCore(comparisonAttribute, comparator, formatUserConditionComparisonValue(comparisonValue, str)); + } + } +} + +EvaluateLogBuilder& EvaluateLogBuilder::appendPrerequisiteFlagCondition(const PrerequisiteFlagCondition& condition, const std::shared_ptr& settings) { + const auto& prerequisiteFlagKey = condition.prerequisiteFlagKey; + + assert(settings); + const auto it = settings->find(prerequisiteFlagKey); + const char* $prerequisiteFlagKeyCStr = it != settings->end() ? prerequisiteFlagKey.c_str() : kInvalidReferencePlaceholder; + + string str; + return appendFormat("Flag '%s' %s '%s'", + $prerequisiteFlagKeyCStr, getPrerequisiteFlagComparatorText(condition.comparator), formatSettingValue(condition.comparisonValue, str).c_str()); + +} + +EvaluateLogBuilder& EvaluateLogBuilder::appendSegmentCondition(const SegmentCondition& condition, const std::shared_ptr& segments) { + const auto segmentIndex = condition.segmentIndex; + const auto segmentPtr = segmentIndex < 0 || (segments ? segments->size() : 0) <= segmentIndex ? nullptr : &(*segments)[segmentIndex]; + + const char* segmentNameCStr; + if (segmentPtr) segmentNameCStr = !segmentPtr->name.empty() ? segmentPtr->name.c_str() : kInvalidNamePlaceholder; + else segmentNameCStr = kInvalidReferencePlaceholder; + + return appendFormat("User %s '%s'", getSegmentComparatorText(condition.comparator), segmentNameCStr); +} + +EvaluateLogBuilder& EvaluateLogBuilder::appendConditionConsequence(bool result) { + append(" => ").appendConditionResult(result); + + return result ? *this : append(", skipping the remaining AND conditions"); +} + +EvaluateLogBuilder& EvaluateLogBuilder::appendTargetingRuleThenPart(const TargetingRule& targetingRule, SettingType settingType, bool newLine) { + (newLine ? this->newLine() : append(" ")) + .append("THEN"); + + const auto simpleValuePtr = get_if(&targetingRule.then); + if (simpleValuePtr) { + string str; + return appendFormat(" '%s'", formatSettingValue(simpleValuePtr->value, str).c_str()); + } + else { + return append(" % options"); + } +} + +EvaluateLogBuilder& EvaluateLogBuilder::appendTargetingRuleConsequence(const TargetingRule& targetingRule, SettingType settingType, const std::variant& isMatchOrError, bool newLine) { + increaseIndent(); + + const auto isMatchPtr = get_if(&isMatchOrError); + appendTargetingRuleThenPart(targetingRule, settingType, newLine) + .append(" => ").append(isMatchPtr + ? (*isMatchPtr ? "MATCH, applying rule" : "no match") + : get(isMatchOrError)); + + return decreaseIndent(); +} + +const char* getSettingTypeText(SettingType settingType) { + static constexpr std::array kTexts{ + "Boolean", + "String", + "Int", + "Double", + }; + + return kTexts.at(static_cast(settingType)); +} + +const char* getSettingValueTypeText(const SettingValue& settingValue) { + static constexpr std::array kTexts{ + "std::nullopt", + "bool", + "std::string", + "int32_t", + "double", + }; + + return kTexts.at(settingValue.index()); +} + +const char* getUserComparatorText(UserComparator comparator) { + switch (comparator) { + case UserComparator::TextIsOneOf: + case UserComparator::SensitiveTextIsOneOf: + case UserComparator::SemVerIsOneOf: return "IS ONE OF"; + + case UserComparator::TextIsNotOneOf: + case UserComparator::SensitiveTextIsNotOneOf: + case UserComparator::SemVerIsNotOneOf: return "IS NOT ONE OF"; + + case UserComparator::TextContainsAnyOf: return "CONTAINS ANY OF"; + + case UserComparator::TextNotContainsAnyOf: return "NOT CONTAINS ANY OF"; + + case UserComparator::SemVerLess: + case UserComparator::NumberLess: return "<"; + + case UserComparator::SemVerLessOrEquals: + case UserComparator::NumberLessOrEquals: return "<="; + + case UserComparator::SemVerGreater: + case UserComparator::NumberGreater: return ">"; + + case UserComparator::SemVerGreaterOrEquals: + case UserComparator::NumberGreaterOrEquals: return ">="; + + case UserComparator::NumberEquals: return "="; + + case UserComparator::NumberNotEquals: return "!="; + + case UserComparator::DateTimeBefore: return "BEFORE"; + + case UserComparator::DateTimeAfter: return "AFTER"; + + case UserComparator::TextEquals: + case UserComparator::SensitiveTextEquals: return "EQUALS"; + + case UserComparator::TextNotEquals: + case UserComparator::SensitiveTextNotEquals: return "NOT EQUALS"; + + case UserComparator::TextStartsWithAnyOf: + case UserComparator::SensitiveTextStartsWithAnyOf: return "STARTS WITH ANY OF"; + + case UserComparator::TextNotStartsWithAnyOf: + case UserComparator::SensitiveTextNotStartsWithAnyOf: return "NOT STARTS WITH ANY OF"; + + case UserComparator::TextEndsWithAnyOf: + case UserComparator::SensitiveTextEndsWithAnyOf: return "ENDS WITH ANY OF"; + + case UserComparator::TextNotEndsWithAnyOf: + case UserComparator::SensitiveTextNotEndsWithAnyOf: return "NOT ENDS WITH ANY OF"; + + case UserComparator::ArrayContainsAnyOf: + case UserComparator::SensitiveArrayContainsAnyOf: return "ARRAY CONTAINS ANY OF"; + + case UserComparator::ArrayNotContainsAnyOf: + case UserComparator::SensitiveArrayNotContainsAnyOf: return "ARRAY NOT CONTAINS ANY OF"; + + default: return kInvalidOperatorPlaceholder; + } +} + +const char* getPrerequisiteFlagComparatorText(PrerequisiteFlagComparator comparator) { + switch (comparator) { + case PrerequisiteFlagComparator::Equals: return "EQUALS"; + case PrerequisiteFlagComparator::NotEquals: return "NOT EQUALS"; + default: return kInvalidOperatorPlaceholder; + } +} + +const char* getSegmentComparatorText(SegmentComparator comparator) { + switch (comparator) { + case SegmentComparator::IsIn: return "IS IN SEGMENT"; + case SegmentComparator::IsNotIn: return "IS NOT IN SEGMENT"; + default: return kInvalidOperatorPlaceholder; + } +} + +const std::string& formatSettingValue(const SettingValue& settingValue, std::string& str) { + if (const auto textPtr = get_if(&settingValue)) { + // Avoid copy if we already have a string. + return *textPtr; + } + else { + auto value = static_cast>(settingValue); + return str = value ? value->toString() : kInvalidValuePlaceholder; + } +} + +const std::string& formatUserConditionComparisonValue(const UserConditionComparisonValue& comparisonValue, std::string& str) { + if (const auto textPtr = get_if(&comparisonValue)) { + // Avoid copy if we already have a string. + return *textPtr; + } + else if (const auto numberPtr = get_if(&comparisonValue)) { + return str = to_string(*numberPtr); + } + else if (const auto stringArrayPtr = get_if>(&comparisonValue)) { + ostringstream ss; + ss << "["; + append_stringlist(ss, *stringArrayPtr); + ss << "]"; + return str = ss.str(); + } + else { + return str = kInvalidValuePlaceholder; + } +} + +std::string formatUserCondition(const UserCondition& condition) { + EvaluateLogBuilder logBuilder; + logBuilder.appendUserCondition(condition); + return logBuilder.toString(); +} + +} // namespace configcat diff --git a/src/evaluatelogbuilder.h b/src/evaluatelogbuilder.h new file mode 100644 index 0000000..04a7105 --- /dev/null +++ b/src/evaluatelogbuilder.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include + +#include "configcat/config.h" +#include "configcat/configcatuser.h" +#include "utils.h" + +namespace configcat { + +class EvaluateLogBuilder { + std::ostringstream ss; + int indentLevel; + +public: + EvaluateLogBuilder() : indentLevel(0) {} + + inline EvaluateLogBuilder& resetIndent() { + this->indentLevel = 0; + return *this; + } + + inline EvaluateLogBuilder& increaseIndent() { + this->indentLevel++; + return *this; + } + + inline EvaluateLogBuilder& decreaseIndent() { + assert(this->indentLevel > 0); + this->indentLevel--; + return *this; + } + + EvaluateLogBuilder& newLine(); + + template + EvaluateLogBuilder& newLine(const ValueType& value) { + return newLine().append(value); + } + + template + EvaluateLogBuilder& append(const ValueType& value) { + this->ss << value; + return *this; + } + + template + inline EvaluateLogBuilder& appendFormat(const std::string& format, Args... args) { + this->ss << string_format(format, args...); + return *this; + } + + EvaluateLogBuilder& appendUserCondition(const UserCondition& condition); + EvaluateLogBuilder& appendPrerequisiteFlagCondition(const PrerequisiteFlagCondition& condition, const std::shared_ptr& settings); + EvaluateLogBuilder& appendSegmentCondition(const SegmentCondition& condition, const std::shared_ptr& segments); + + inline EvaluateLogBuilder& appendConditionResult(bool result) { return append(result ? "true" : "false"); } + EvaluateLogBuilder& appendConditionConsequence(bool result); + + EvaluateLogBuilder& appendTargetingRuleConsequence(const TargetingRule& targetingRule, SettingType settingType, const std::variant& isMatchOrError, bool newLine); + + inline std::string toString() const { return ss.str(); } +private: + EvaluateLogBuilder& appendUserConditionCore(const std::string& comparisonAttribute, UserComparator comparator, const std::optional& comparisonValue); + EvaluateLogBuilder& appendUserConditionString(const std::string& comparisonAttribute, UserComparator comparator, const UserConditionComparisonValue& comparisonValue, bool isSensitive); + EvaluateLogBuilder& appendUserConditionStringList(const std::string& comparisonAttribute, UserComparator comparator, const UserConditionComparisonValue& comparisonValue, bool isSensitive); + EvaluateLogBuilder& appendUserConditionNumber(const std::string& comparisonAttribute, UserComparator comparator, const UserConditionComparisonValue& comparisonValue, bool isDateTime); + + EvaluateLogBuilder& appendTargetingRuleThenPart(const TargetingRule& targetingRule, SettingType settingType, bool newLine); +}; + +const char* getSettingTypeText(SettingType settingType); +const char* getSettingValueTypeText(const SettingValue& settingValue); +const char* getUserComparatorText(UserComparator comparator); +const char* getPrerequisiteFlagComparatorText(PrerequisiteFlagComparator comparator); +const char* getSegmentComparatorText(SegmentComparator comparator); + +const std::string& formatSettingValue(const SettingValue& settingValue, std::string& str); +const std::string& formatUserConditionComparisonValue(const UserConditionComparisonValue& comparisonValue, std::string& str); +std::string formatUserCondition(const UserCondition& condition); + +} // namespace configcat diff --git a/src/fileoverridedatasource.cpp b/src/fileoverridedatasource.cpp index 5d3114a..5e03abc 100644 --- a/src/fileoverridedatasource.cpp +++ b/src/fileoverridedatasource.cpp @@ -1,5 +1,5 @@ #include "configcat/fileoverridedatasource.h" -#include "configcat/configcatlogger.h" +#include "configcatlogger.h" using namespace std; @@ -10,11 +10,11 @@ FileFlagOverrides::FileFlagOverrides(const std::string& filePath, OverrideBehavi behaviour(behaviour) { } -std::shared_ptr FileFlagOverrides::createDataSource(std::shared_ptr logger) { +std::shared_ptr FileFlagOverrides::createDataSource(const std::shared_ptr& logger) { return make_shared(filePath, behaviour, logger); } -FileOverrideDataSource::FileOverrideDataSource(const string& filePath, OverrideBehaviour behaviour, std::shared_ptr logger): +FileOverrideDataSource::FileOverrideDataSource(const string& filePath, OverrideBehaviour behaviour, const std::shared_ptr& logger): OverrideDataSource(behaviour), overrides(make_shared>()), filePath(filePath), @@ -27,7 +27,7 @@ FileOverrideDataSource::FileOverrideDataSource(const string& filePath, OverrideB } } -const shared_ptr> FileOverrideDataSource::getOverrides() { +shared_ptr> FileOverrideDataSource::getOverrides() { reloadFileContent(); return overrides; } @@ -38,12 +38,14 @@ void FileOverrideDataSource::reloadFileContent() { if (fileLastWriteTime != lastWriteTime) { fileLastWriteTime = lastWriteTime; auto config = Config::fromFile(filePath); - overrides = config->entries; + overrides = config->getSettingsOrEmpty(); } - } catch (filesystem::filesystem_error exception) { - LOG_ERROR(1302) << "Failed to read the local config file '" << filePath << "'. " << exception.what(); - } catch (exception& exception) { - LOG_ERROR(2302) << "Failed to decode JSON from the local config file '" << filePath << "'. " << exception.what(); + } catch (const filesystem::filesystem_error&) { + LogEntry logEntry(logger, configcat::LOG_LEVEL_ERROR, 1302, current_exception()); + logEntry << "Failed to read the local config file '" << filePath << "'."; + } catch (...) { + LogEntry logEntry(logger, configcat::LOG_LEVEL_ERROR, 2302, current_exception()); + logEntry << "Failed to decode JSON from the local config file '" << filePath << "'."; } } diff --git a/src/mapoverridedatasource.cpp b/src/mapoverridedatasource.cpp index 54bc44b..3971d78 100644 --- a/src/mapoverridedatasource.cpp +++ b/src/mapoverridedatasource.cpp @@ -7,12 +7,12 @@ namespace configcat { MapFlagOverrides::MapFlagOverrides(const std::unordered_map& source, OverrideBehaviour behaviour): overrides(make_shared()), behaviour(behaviour) { - for (const auto& it : source) { - overrides->insert({it.first, {it.second}}); + for (const auto& [key, value] : source) { + overrides->insert({key, Setting::fromValue(value)}); } } -std::shared_ptr MapFlagOverrides::createDataSource(std::shared_ptr logger) { +std::shared_ptr MapFlagOverrides::createDataSource(const std::shared_ptr& logger) { return make_shared(overrides, behaviour); } diff --git a/src/rolloutevaluator.cpp b/src/rolloutevaluator.cpp index 4760d5f..d940db1 100644 --- a/src/rolloutevaluator.cpp +++ b/src/rolloutevaluator.cpp @@ -1,292 +1,1074 @@ -#include "rolloutevaluator.h" -#include "configcat/configcatuser.h" -#include "configcat/configcatlogger.h" -#include "utils.h" -#include +#include +#include +#include // must be imported before #include + #include +#include +#include #include +#include "configcatlogger.h" +#include "rolloutevaluator.h" + using namespace std; namespace configcat { -RolloutEvaluator::RolloutEvaluator(std::shared_ptr logger): - logger(logger), - sha1(make_unique()) { +static constexpr char kTargetingRuleIgnoredMessage[] = "The current targeting rule is ignored and the evaluation continues with the next rule."; +static constexpr char kMissingUserObjectError[] = "cannot evaluate, User Object is missing"; +static constexpr char kMissingUserAttributeError[] = "cannot evaluate, the User.%s attribute is missing"; +static constexpr char kInvalidUserAttributeError[] = "cannot evaluate, the User.%s attribute is invalid (%s)"; + +static const string& ensureConfigJsonSalt(const shared_ptr& value) { + if (value) { + return *value; + } + throw runtime_error("Config JSON salt is missing."); } -RolloutEvaluator::~RolloutEvaluator() { +template +static const ValueType& ensureComparisonValue(const UserConditionComparisonValue& comparisonValue) { + if (const auto comparisonValuePtr = get_if(&comparisonValue); comparisonValuePtr) { + return *comparisonValuePtr; + } + throw runtime_error("Comparison value is missing or invalid."); } -tuple RolloutEvaluator::evaluate( - const string& key, - const ConfigCatUser* user, - const Setting& setting) { - LogEntry logEntry(logger, LOG_LEVEL_INFO, 5000); - logEntry << "Evaluating getValue(" << key << ")"; +static string hashComparisonValue(SHA256& sha256, const string& value, const string& configJsonSalt, const string& contextSalt) { + return sha256(value + configJsonSalt + contextSalt); +} - if (!user) { - if (!setting.rolloutRules.empty() || !setting.percentageItems.empty()) { - LOG_WARN(3001) << - "Cannot evaluate targeting rules and % options for setting '" << key << "' (User Object is missing). " - "You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. " - "Read more: https://configcat.com/docs/advanced/user-object/"; - } +RolloutEvaluator::RolloutEvaluator(const std::shared_ptr& logger) : + logger(logger), + sha1(make_unique()), + sha256(make_unique()) { +} - logEntry << "\n" << "Returning " << setting.value; - return {setting.value, setting.variationId, {}, {}, logEntry.getMessage()}; - } +EvaluateResult RolloutEvaluator::evaluate(const std::optional& defaultValue, EvaluateContext& context, std::optional& returnValue) const { + auto& logBuilder = context.logBuilder; - logEntry << "\n" << "User object: " << user; + // Building the evaluation log is expensive, so let's not do it if it wouldn't be logged anyway. + if (logger->isEnabled(LogLevel::LOG_LEVEL_INFO)) { + logBuilder = make_shared(); - for (const auto& rule : setting.rolloutRules) { - const auto& comparisonAttribute = rule.comparisonAttribute; - const auto& comparisonValue = rule.comparisonValue; - const auto& comparator = rule.comparator; - const auto* userValuePtr = user->getAttribute(comparisonAttribute); - const auto& returnValue = rule.value; + logBuilder->appendFormat("Evaluating '%s'", context.key.c_str()); - if (userValuePtr == nullptr || comparisonValue.empty() || userValuePtr->empty()) { - logEntry << "\n" << formatNoMatchRule(comparisonAttribute, userValuePtr ? *userValuePtr : "null", comparator, comparisonValue); - continue; + if (context.user) { + logBuilder->appendFormat(" for User '%s'", context.user->toJson().c_str()); } - const string& userValue = *userValuePtr; + logBuilder->increaseIndent(); + } - switch (comparator) { - case ONE_OF: { - stringstream stream(comparisonValue); - string token; - while (getline(stream, token, ',')) { - trim(token); - if (token == userValue) { - logEntry << "\n" << formatMatchRule(comparisonAttribute, userValue, comparator, comparisonValue, returnValue); - return {rule.value, rule.variationId, &rule, {}, {}}; - } - } - break; - } - case NOT_ONE_OF: { - stringstream stream(comparisonValue); - string token; - bool found = false; - while (getline(stream, token, ',')) { - trim(token); - if (token == userValue) { - found = true; - break; - } - } - if (!found) { - logEntry << "\n" << formatMatchRule(comparisonAttribute, userValue, comparator, comparisonValue, returnValue); - return {rule.value, rule.variationId, &rule, {}, {}}; - } - break; - } - case CONTAINS: { - if (userValue.find(comparisonValue) != std::string::npos) { - logEntry << "\n" << formatMatchRule(comparisonAttribute, userValue, comparator, comparisonValue, returnValue); - return {rule.value, rule.variationId, &rule, {}, {}}; - } - break; + const auto log = []( + const std::optional& returnValue, + const shared_ptr& logger, + const shared_ptr& logBuilder + ) { + if (logBuilder) { + logBuilder->newLine().appendFormat("Returning '%s'.", returnValue ? returnValue->toString().c_str() : ""); + logBuilder->decreaseIndent(); + + logger->log(LogLevel::LOG_LEVEL_INFO, 5000, logBuilder->toString()); } - case NOT_CONTAINS: { - if (userValue.find(comparisonValue) == std::string::npos) { - logEntry << "\n" << formatMatchRule(comparisonAttribute, userValue, comparator, comparisonValue, returnValue); - return {rule.value, rule.variationId, &rule, {}, {}}; - } - break; + }; + + try { + const auto settingType = context.setting.getTypeChecked(); + + if (defaultValue) { + const auto expectedSettingType = static_cast(*defaultValue).getSettingType(); + + if (settingType != expectedSettingType) { + const char* settingTypeFormatted = getSettingTypeText(settingType); + const char* defaultValueTypeFormatted = getSettingValueTypeText(static_cast(*defaultValue)); + throw runtime_error(string_format( + "The type of a setting must match the type of the specified default value. " + "Setting's type was %s but the default value's type was %s. " + "Please use a default value which corresponds to the setting type %s. " + "Learn more: https://configcat.com/docs/sdk-reference/cpp/#setting-type-mapping", + settingTypeFormatted, defaultValueTypeFormatted, settingTypeFormatted)); } - case ONE_OF_SEMVER: - case NOT_ONE_OF_SEMVER: { - // The rule will be ignored if we found an invalid semantic version - try { - semver::version userValueVersion = semver::version::parse(userValue); - stringstream stream(comparisonValue); - string token; - bool matched = false; - while (getline(stream, token, ',')) { - trim(token); - - // Filter empty versions - if (token.empty()) - continue; - - semver::version tokenVersion = semver::version::parse(token); - matched = tokenVersion == userValueVersion || matched; - } - - if ((matched && comparator == ONE_OF_SEMVER) || - (!matched && comparator == NOT_ONE_OF_SEMVER)) { - logEntry << "\n" << formatMatchRule(comparisonAttribute, userValue, comparator, comparisonValue, - returnValue); - return {rule.value, rule.variationId, &rule, {}, {}}; - } - } catch (semver::semver_exception& exception) { - auto message = formatValidationErrorRule(comparisonAttribute, userValue, comparator, - comparisonValue, exception.what()); - logEntry << "\n" << message; - LOG_WARN(0) << message; + } + + auto result = evaluateSetting(context); + returnValue = result.selectedValue.value.toValueChecked(settingType); + // At this point it's ensured that the return value is compatible with the default value + // (specifically, with the return type of the evaluation method overload that was called). + + log(returnValue, logger, logBuilder); + return result; + } + catch (...) { + auto ex = current_exception(); + if (logBuilder) logBuilder->resetIndent().increaseIndent(); + + log(defaultValue, logger, logBuilder); + rethrow_exception(ex); + } +} + +EvaluateResult RolloutEvaluator::evaluateSetting(EvaluateContext& context) const { + const auto& targetingRules = context.setting.targetingRules; + if (!targetingRules.empty()) { + auto evaluateResult = evaluateTargetingRules(targetingRules, context); + if (evaluateResult) { + return std::move(*evaluateResult); + } + } + + const auto& percentageOptions = context.setting.percentageOptions; + if (!percentageOptions.empty()) { + auto evaluateResult = evaluatePercentageOptions(percentageOptions, nullptr, context); + if (evaluateResult) { + return std::move(*evaluateResult); + } + } + + return { context.setting, nullptr, nullptr }; +} + +std::optional RolloutEvaluator::evaluateTargetingRules(const std::vector& targetingRules, EvaluateContext& context) const { + const auto& logBuilder = context.logBuilder; + + if (logBuilder) logBuilder->newLine("Evaluating targeting rules and applying the first match if any:"); + + const auto conditionAccessor = std::function([](const ConditionContainer& container) -> const Condition& { return container.condition; }); + + for (const auto& targetingRule : targetingRules) { + const std::vector& conditions = targetingRule.conditions; + + const auto isMatchOrError = evaluateConditions(conditions, conditionAccessor, &targetingRule, context.key, context); + + if (const auto isMatchPtr = get_if(&isMatchOrError); !isMatchPtr || !*isMatchPtr) { + if (!isMatchPtr) { + if (logBuilder) { + logBuilder->increaseIndent() + .newLine(kTargetingRuleIgnoredMessage) + .decreaseIndent(); } - break; } - case LT_SEMVER: - case LTE_SEMVER: - case GT_SEMVER: - case GTE_SEMVER: { - // The rule will be ignored if we found an invalid semantic version - try { - string userValueCopy = userValue; - semver::version userValueVersion = semver::version::parse(userValueCopy); - - string comparisonValueCopy = comparisonValue; - semver::version comparisonValueVersion = semver::version::parse(trim(comparisonValueCopy)); - - if ((comparator == LT_SEMVER && userValueVersion < comparisonValueVersion) || - (comparator == LTE_SEMVER && userValueVersion <= comparisonValueVersion) || - (comparator == GT_SEMVER && userValueVersion > comparisonValueVersion) || - (comparator == GTE_SEMVER && userValueVersion >= comparisonValueVersion)) { - logEntry << "\n" << formatMatchRule(comparisonAttribute, userValue, comparator, comparisonValue, - returnValue); - return {rule.value, rule.variationId, &rule, {}, {}}; - } - } catch (semver::semver_exception& exception) { - auto message = formatValidationErrorRule(comparisonAttribute, userValue, comparator, - comparisonValue, exception.what()); - logEntry << "\n" << message; - LOG_WARN(0) << message; - } - break; + continue; + } + + if (const auto simpleValuePtr = get_if(&targetingRule.then); simpleValuePtr) { + return EvaluateResult{ *simpleValuePtr, &targetingRule, nullptr }; + } + + const auto percentageOptionsPtr = get_if(&targetingRule.then); + if (!percentageOptionsPtr || percentageOptionsPtr->empty()) { + throw runtime_error("Targeting rule THEN part is missing or invalid."); + } + + if (logBuilder) logBuilder->increaseIndent(); + + auto evaluateResult = evaluatePercentageOptions(*percentageOptionsPtr, &targetingRule, context); + if (evaluateResult) { + if (logBuilder) logBuilder->decreaseIndent(); + + return std::move(*evaluateResult); + } + + if (logBuilder) { + logBuilder->newLine(kTargetingRuleIgnoredMessage) + .decreaseIndent(); + } + } + + return nullopt; +} + +std::optional RolloutEvaluator::evaluatePercentageOptions(const std::vector& percentageOptions, const TargetingRule* matchedTargetingRule, EvaluateContext& context) const { + const auto& logBuilder = context.logBuilder; + + if (!context.user) { + if (logBuilder) logBuilder->newLine("Skipping % options because the User Object is missing."); + + if (!context.isMissingUserObjectLogged) { + logUserObjectIsMissing(context.key); + context.isMissingUserObjectLogged = true; + } + + return nullopt; + } + + const auto& percentageOptionsAttributeName = context.setting.percentageOptionsAttribute; + const auto percentageOptionsAttributeValuePtr = percentageOptionsAttributeName + ? context.user->getAttribute(percentageOptionsAttributeName.value_or(ConfigCatUser::kIdentifierAttribute)) + : &context.user->getIdentifierAttribute(); + + if (!percentageOptionsAttributeValuePtr) { + if (logBuilder) { + logBuilder->newLine().appendFormat("Skipping %% options because the User.%s attribute is missing.", + percentageOptionsAttributeName ? percentageOptionsAttributeName->c_str() : ConfigCatUser::kIdentifierAttribute); + } + + if (!context.isMissingUserObjectAttributeLogged) { + logUserObjectAttributeIsMissingPercentage(context.key, + percentageOptionsAttributeName ? percentageOptionsAttributeName->c_str() : ConfigCatUser::kIdentifierAttribute); + context.isMissingUserObjectAttributeLogged = true; + } + + return nullopt; + } + + if (logBuilder) { + logBuilder->newLine().appendFormat("Evaluating %% options based on the User.%s attribute:", + percentageOptionsAttributeName ? percentageOptionsAttributeName->c_str() : ConfigCatUser::kIdentifierAttribute); + } + + const auto userAttributeValuePtr = get_if(percentageOptionsAttributeValuePtr); + const auto userAttributeValue = userAttributeValuePtr ? string() : userAttributeValueToString(*percentageOptionsAttributeValuePtr); + + auto sha1 = (*this->sha1)(context.key + (userAttributeValuePtr ? *userAttributeValuePtr : userAttributeValue)); + const auto hashValue = std::stoul(sha1.erase(7), nullptr, 16) % 100; + + if (logBuilder) { + logBuilder->newLine().appendFormat("- Computing hash in the [0..99] range from User.%s => %d (this value is sticky and consistent across all SDKs)", + percentageOptionsAttributeName ? percentageOptionsAttributeName->c_str() : ConfigCatUser::kIdentifierAttribute, hashValue); + } + + auto bucket = 0u; + auto optionNumber = 1; + + for (const auto& percentageOption : percentageOptions) { + auto percentage = percentageOption.percentage; + bucket += percentage; + + if (hashValue >= bucket) { + ++optionNumber; + continue; + } + + if (logBuilder) { + string str; + logBuilder->newLine().appendFormat("- Hash value %d selects %% option %d (%d%%), '%s'.", + hashValue, optionNumber, percentage, formatSettingValue(percentageOption.value, str).c_str()); + } + + return EvaluateResult{ percentageOption, matchedTargetingRule, &percentageOption }; + } + + throw runtime_error("Sum of percentage option percentages is less than 100."); +} + +template +RolloutEvaluator::SuccessOrError RolloutEvaluator::evaluateConditions(const std::vector& conditions, const std::function& conditionAccessor, + const TargetingRule* targetingRule, const std::string& contextSalt, EvaluateContext& context) const { + + RolloutEvaluator::SuccessOrError result = true; + + const auto& logBuilder = context.logBuilder; + auto newLineBeforeThen = false; + + if (logBuilder) logBuilder->newLine("- "); + + auto i = 0; + for (const auto& it : conditions) { + const ConditionType& condition = conditionAccessor(it); + + if (logBuilder) { + if (i == 0) { + logBuilder->append("IF ") + .increaseIndent(); + } else { + logBuilder->increaseIndent() + .newLine("AND "); } - case EQ_NUM: - case NOT_EQ_NUM: - case LT_NUM: - case LTE_NUM: - case GT_NUM: - case GTE_NUM: { - bool error = false; - double userValueDouble = str_to_double(userValue, error); - if (error) { - string reason = string_format("Cannot convert string \"%s\" to double.", userValue.c_str()); - auto message = formatValidationErrorRule(comparisonAttribute, userValue, comparator, comparisonValue, reason); - logEntry << "\n" << message; - LOG_WARN(0) << message; - break; - } + } - double comparisonValueDouble = str_to_double(comparisonValue, error); - if (error) { - string reason = string_format("Cannot convert string \"%s\" to double.", comparisonValue.c_str()); - auto message = formatValidationErrorRule(comparisonAttribute, userValue, comparator, comparisonValue, reason); - logEntry << "\n" << message; - LOG_WARN(0) << message; - break; - } + if (const auto userConditionPtr = get_if(&condition); userConditionPtr) { + result = evaluateUserCondition(*userConditionPtr, contextSalt, context); + newLineBeforeThen = conditions.size() > 1; + } else if (const auto prerequisiteFlagConditionPtr = get_if(&condition); prerequisiteFlagConditionPtr) { + result = evaluatePrerequisiteFlagCondition(*prerequisiteFlagConditionPtr, context); + newLineBeforeThen = true; + } else if (const auto segmentConditionPtr = get_if(&condition); segmentConditionPtr) { + result = evaluateSegmentCondition(*segmentConditionPtr, context); + newLineBeforeThen = !holds_alternative(result) || get(result) != kMissingUserObjectError || conditions.size() > 1; + } else { + throw runtime_error("Condition is missing or invalid."); + } - if (comparator == EQ_NUM && userValueDouble == comparisonValueDouble || - comparator == NOT_EQ_NUM && userValueDouble != comparisonValueDouble || - comparator == LT_NUM && userValueDouble < comparisonValueDouble || - comparator == LTE_NUM && userValueDouble <= comparisonValueDouble || - comparator == GT_NUM && userValueDouble > comparisonValueDouble || - comparator == GTE_NUM && userValueDouble >= comparisonValueDouble) { - logEntry << "\n" << formatMatchRule(comparisonAttribute, userValue, comparator, comparisonValue, returnValue); - return {rule.value, rule.variationId, &rule, {}, {}}; - } - break; + const auto successPtr = get_if(&result); + const auto success = successPtr && *successPtr; + + if (logBuilder) { + if (!targetingRule || conditions.size() > 1) { + logBuilder->appendConditionConsequence(success); } - case ONE_OF_SENS: { - stringstream stream(comparisonValue); - string token; - auto userValueHash = (*sha1)(userValue); - while (getline(stream, token, ',')) { - trim(token); - if (token == userValueHash) { - logEntry << "\n" << formatMatchRule(comparisonAttribute, userValue, comparator, comparisonValue, returnValue); - return {rule.value, rule.variationId, &rule, {}, {}}; - } - } - break; + + logBuilder->decreaseIndent(); + } + + if (!success) break; + + ++i; + } + + if (targetingRule) { + if (logBuilder) logBuilder->appendTargetingRuleConsequence(*targetingRule, context.setting.type, result, newLineBeforeThen); + } + + return result; +} + +RolloutEvaluator::SuccessOrError RolloutEvaluator::evaluateUserCondition(const UserCondition& condition, const std::string& contextSalt, EvaluateContext& context) const { + const auto& logBuilder = context.logBuilder; + if (logBuilder) logBuilder->appendUserCondition(condition); + + if (!context.user) { + if (!context.isMissingUserObjectLogged) { + logUserObjectIsMissing(context.key); + context.isMissingUserObjectLogged = true; + } + + return string(kMissingUserObjectError); + } + + const auto& userAttributeName = condition.comparisonAttribute; + const auto userAttributeValuePtr = context.user->getAttribute(userAttributeName); + + if (const string* textPtr; !userAttributeValuePtr || (textPtr = get_if(userAttributeValuePtr)) && textPtr->empty()) { + logUserObjectAttributeIsMissingCondition(formatUserCondition(condition), context.key, userAttributeName); + + return string_format(kMissingUserAttributeError, userAttributeName.c_str()); + } + + const auto comparator = condition.comparator; + + switch (comparator) { + case UserComparator::TextEquals: + case UserComparator::TextNotEquals: { + string text; + const auto& textRef = getUserAttributeValueAsText(userAttributeName, *userAttributeValuePtr, condition, context.key, text); + + return evaluateTextEquals( + textRef, + condition.comparisonValue, + comparator == UserComparator::TextNotEquals + ); + } + + case UserComparator::SensitiveTextEquals: + case UserComparator::SensitiveTextNotEquals: { + string text; + const auto& textRef = getUserAttributeValueAsText(userAttributeName, *userAttributeValuePtr, condition, context.key, text); + + return evaluateSensitiveTextEquals( + textRef, + condition.comparisonValue, + ensureConfigJsonSalt(context.setting.configJsonSalt), + contextSalt, + comparator == UserComparator::SensitiveTextNotEquals + ); + } + + case UserComparator::TextIsOneOf: + case UserComparator::TextIsNotOneOf: { + string text; + const auto& textRef = getUserAttributeValueAsText(userAttributeName, *userAttributeValuePtr, condition, context.key, text); + + return evaluateTextIsOneOf( + textRef, + condition.comparisonValue, + comparator == UserComparator::TextIsNotOneOf + ); + } + + case UserComparator::SensitiveTextIsOneOf: + case UserComparator::SensitiveTextIsNotOneOf: { + string text; + const auto& textRef = getUserAttributeValueAsText(userAttributeName, *userAttributeValuePtr, condition, context.key, text); + + return evaluateSensitiveTextIsOneOf( + textRef, + condition.comparisonValue, + ensureConfigJsonSalt(context.setting.configJsonSalt), + contextSalt, + comparator == UserComparator::SensitiveTextIsNotOneOf + ); + } + + case UserComparator::TextStartsWithAnyOf: + case UserComparator::TextNotStartsWithAnyOf: { + string text; + const auto& textRef = getUserAttributeValueAsText(userAttributeName, *userAttributeValuePtr, condition, context.key, text); + + return evaluateTextSliceEqualsAnyOf( + textRef, + condition.comparisonValue, + true, + comparator == UserComparator::TextNotStartsWithAnyOf + ); + } + + case UserComparator::SensitiveTextStartsWithAnyOf: + case UserComparator::SensitiveTextNotStartsWithAnyOf: { + string text; + const auto& textRef = getUserAttributeValueAsText(userAttributeName, *userAttributeValuePtr, condition, context.key, text); + + return evaluateSensitiveTextSliceEqualsAnyOf( + textRef, + condition.comparisonValue, + ensureConfigJsonSalt(context.setting.configJsonSalt), + contextSalt, + true, + comparator == UserComparator::SensitiveTextNotStartsWithAnyOf + ); + } + + case UserComparator::TextEndsWithAnyOf: + case UserComparator::TextNotEndsWithAnyOf: { + string text; + const auto& textRef = getUserAttributeValueAsText(userAttributeName, *userAttributeValuePtr, condition, context.key, text); + + return evaluateTextSliceEqualsAnyOf( + textRef, + condition.comparisonValue, + false, + comparator == UserComparator::TextNotEndsWithAnyOf + ); + } + + case UserComparator::SensitiveTextEndsWithAnyOf: + case UserComparator::SensitiveTextNotEndsWithAnyOf: { + string text; + const auto& textRef = getUserAttributeValueAsText(userAttributeName, *userAttributeValuePtr, condition, context.key, text); + + return evaluateSensitiveTextSliceEqualsAnyOf( + textRef, + condition.comparisonValue, + ensureConfigJsonSalt(context.setting.configJsonSalt), + contextSalt, + false, + comparator == UserComparator::SensitiveTextNotEndsWithAnyOf + ); + } + + case UserComparator::TextContainsAnyOf: + case UserComparator::TextNotContainsAnyOf: { + string text; + const auto& textRef = getUserAttributeValueAsText(userAttributeName, *userAttributeValuePtr, condition, context.key, text); + + return evaluateTextContainsAnyOf( + textRef, + condition.comparisonValue, + comparator == UserComparator::TextNotContainsAnyOf + ); + } + + case UserComparator::SemVerIsOneOf: + case UserComparator::SemVerIsNotOneOf: { + semver::version version; + const auto versionPtrOrError = getUserAttributeValueAsSemVer(userAttributeName, *userAttributeValuePtr, condition, context.key, version); + if (auto errorPtr = get_if(&versionPtrOrError)) { + return std::move(*const_cast(errorPtr)); + } + + return evaluateSemVerIsOneOf( + *get(versionPtrOrError), + condition.comparisonValue, + comparator == UserComparator::SemVerIsNotOneOf + ); + } + + case UserComparator::SemVerLess: + case UserComparator::SemVerLessOrEquals: + case UserComparator::SemVerGreater: + case UserComparator::SemVerGreaterOrEquals: { + semver::version version; + const auto versionPtrOrError = getUserAttributeValueAsSemVer(userAttributeName, *userAttributeValuePtr, condition, context.key, version); + if (auto errorPtr = get_if(&versionPtrOrError)) { + return std::move(*const_cast(errorPtr)); + } + + return evaluateSemVerRelation( + *get(versionPtrOrError), + comparator, + condition.comparisonValue + ); + } + + case UserComparator::NumberEquals: + case UserComparator::NumberNotEquals: + case UserComparator::NumberLess: + case UserComparator::NumberLessOrEquals: + case UserComparator::NumberGreater: + case UserComparator::NumberGreaterOrEquals: { + const auto numberOrError = getUserAttributeValueAsNumber(userAttributeName, *userAttributeValuePtr, condition, context.key); + if (auto errorPtr = get_if(&numberOrError)) { + return std::move(*const_cast(errorPtr)); + } + + return evaluateNumberRelation( + get(numberOrError), + comparator, + condition.comparisonValue + ); + } + + case UserComparator::DateTimeBefore: + case UserComparator::DateTimeAfter: { + const auto numberOrError = getUserAttributeValueAsUnixTimeSeconds(userAttributeName, *userAttributeValuePtr, condition, context.key); + if (auto errorPtr = get_if(&numberOrError)) { + return std::move(*const_cast(errorPtr)); + } + + return evaluateDateTimeRelation( + get(numberOrError), + condition.comparisonValue, + comparator == UserComparator::DateTimeBefore + ); + } + + case UserComparator::ArrayContainsAnyOf: + case UserComparator::ArrayNotContainsAnyOf: { + vector array; + const auto arrayPtrOrError = getUserAttributeValueAsStringArray(userAttributeName, *userAttributeValuePtr, condition, context.key, array); + if (auto errorPtr = get_if(&arrayPtrOrError)) { + return std::move(*const_cast(errorPtr)); + } + + return evaluateArrayContainsAnyOf( + *get*>(arrayPtrOrError), + condition.comparisonValue, + comparator == UserComparator::ArrayNotContainsAnyOf + ); + } + + case UserComparator::SensitiveArrayContainsAnyOf: + case UserComparator::SensitiveArrayNotContainsAnyOf: { + vector array; + const auto arrayPtrOrError = getUserAttributeValueAsStringArray(userAttributeName, *userAttributeValuePtr, condition, context.key, array); + if (auto errorPtr = get_if(&arrayPtrOrError)) { + return std::move(*const_cast(errorPtr)); + } + + return evaluateSensitiveArrayContainsAnyOf( + *get*>(arrayPtrOrError), + condition.comparisonValue, + ensureConfigJsonSalt(context.setting.configJsonSalt), + contextSalt, + comparator == UserComparator::SensitiveArrayNotContainsAnyOf + ); + } + + default: + throw runtime_error("Comparison operator is invalid."); + } +} + +bool RolloutEvaluator::evaluateTextEquals(const std::string& text, const UserConditionComparisonValue& comparisonValue, bool negate) const { + const auto& text2 = ensureComparisonValue(comparisonValue); + + return (text == text2) ^ negate; +} + +bool RolloutEvaluator::evaluateSensitiveTextEquals(const std::string& text, const UserConditionComparisonValue& comparisonValue, const std::string& configJsonSalt, const std::string& contextSalt, bool negate) const { + const auto& hash2 = ensureComparisonValue(comparisonValue); + + const auto hash = hashComparisonValue(*sha256, text, configJsonSalt, contextSalt); + + return (hash == hash2) ^ negate; +} + +bool RolloutEvaluator::evaluateTextIsOneOf(const std::string& text, const UserConditionComparisonValue& comparisonValue, bool negate) const { + const auto& comparisonValues = ensureComparisonValue>(comparisonValue); + + for (const auto& comparisonValue : comparisonValues) { + if (text == comparisonValue) { + return !negate; + } + } + + return negate; +} + +bool RolloutEvaluator::evaluateSensitiveTextIsOneOf(const std::string& text, const UserConditionComparisonValue& comparisonValue, const std::string& configJsonSalt, const std::string& contextSalt, bool negate) const { + const auto& comparisonValues = ensureComparisonValue>(comparisonValue); + + const auto hash = hashComparisonValue(*sha256, text, configJsonSalt, contextSalt); + + for (const auto& comparisonValue : comparisonValues) { + if (hash == comparisonValue) { + return !negate; + } + } + + return negate; +} + +bool RolloutEvaluator::evaluateTextSliceEqualsAnyOf(const std::string& text, const UserConditionComparisonValue& comparisonValue, bool startsWith, bool negate) const { + const auto& comparisonValues = ensureComparisonValue>(comparisonValue); + + for (const auto& comparisonValue : comparisonValues) { + const auto success = startsWith ? starts_with(text, comparisonValue) : ends_with(text, comparisonValue); + + if (success) { + return !negate; + } + } + + return negate; +} + +bool RolloutEvaluator::evaluateSensitiveTextSliceEqualsAnyOf(const std::string& text, const UserConditionComparisonValue& comparisonValue, const std::string& configJsonSalt, const std::string& contextSalt, bool startsWith, bool negate) const { + const auto& comparisonValues = ensureComparisonValue>(comparisonValue); + + const auto textLength = text.size(); + + for (const auto& comparisonValue : comparisonValues) { + const auto index = comparisonValue.find('_'); + + size_t sliceLength; + if (index == string::npos + || (sliceLength = integer_from_string(comparisonValue.substr(0, index)).value_or(-1)) < 0) { + throw runtime_error("Comparison value is missing or invalid."); + } + + const string hash2(comparisonValue.substr(index + 1)); + if (hash2.empty()) { + throw runtime_error("Comparison value is missing or invalid."); + } + + if (textLength < sliceLength) { + continue; + } + + const auto slice = startsWith ? text.substr(0, sliceLength) : text.substr(textLength - sliceLength); + + const auto hash = hashComparisonValue(*sha256, slice, configJsonSalt, contextSalt); + if (hash == hash2) { + return !negate; + } + } + + return negate; +} + +bool RolloutEvaluator::evaluateTextContainsAnyOf(const std::string& text, const UserConditionComparisonValue& comparisonValue, bool negate) const { + const auto& comparisonValues = ensureComparisonValue>(comparisonValue); + + for (const auto& comparisonValue : comparisonValues) { + if (contains(text, comparisonValue)) { + return !negate; + } + } + + return negate; +} + +bool RolloutEvaluator::evaluateSemVerIsOneOf(const semver::version& version, const UserConditionComparisonValue& comparisonValue, bool negate) const { + const auto& comparisonValues = ensureComparisonValue>(comparisonValue); + + auto result = false; + + for (auto comparisonValue : comparisonValues) { + // NOTE: Previous versions of the evaluation algorithm ignore empty comparison values. + // We keep this behavior for backward compatibility. + if (comparisonValue.empty()) { + continue; + } + + semver::version version2; + try { + trim(comparisonValue); + version2 = semver::version::parse(comparisonValue); + } + catch (const semver::semver_exception&) { + // NOTE: Previous versions of the evaluation algorithm ignored invalid comparison values. + // We keep this behavior for backward compatibility. + return false; + } + + if (!result && version == version2) { + // NOTE: Previous versions of the evaluation algorithm require that + // none of the comparison values are empty or invalid, that is, we can't stop when finding a match. + // We keep this behavior for backward compatibility. + result = true; + } + } + + return result ^ negate; +} + +bool RolloutEvaluator::evaluateSemVerRelation(const semver::version& version, UserComparator comparator, const UserConditionComparisonValue& comparisonValue) const { + auto comparisonValueStr = ensureComparisonValue(comparisonValue); + + semver::version version2; + try { + trim(comparisonValueStr); + version2 = semver::version::parse(comparisonValueStr); + } + catch (const semver::semver_exception&) { + // NOTE: Previous versions of the evaluation algorithm ignored invalid comparison values. + // We keep this behavior for backward compatibility. + return false; + } + + switch (comparator) { + case UserComparator::SemVerLess: return version < version2; + case UserComparator::SemVerLessOrEquals: return version <= version2; + case UserComparator::SemVerGreater: return version > version2; + case UserComparator::SemVerGreaterOrEquals: return version >= version2; + default: throw logic_error("Non-exhaustive switch."); + } +} + +bool RolloutEvaluator::evaluateNumberRelation(double number, UserComparator comparator, const UserConditionComparisonValue& comparisonValue) const { + const auto number2 = ensureComparisonValue(comparisonValue); + + switch (comparator) { + case UserComparator::NumberEquals: return number == number2; + case UserComparator::NumberNotEquals: return number != number2; + case UserComparator::NumberLess: return number < number2; + case UserComparator::NumberLessOrEquals: return number <= number2; + case UserComparator::NumberGreater: return number > number2; + case UserComparator::NumberGreaterOrEquals: return number >= number2; + default: throw logic_error("Non-exhaustive switch."); + } +} + +bool RolloutEvaluator::evaluateDateTimeRelation(double number, const UserConditionComparisonValue& comparisonValue, bool before) const +{ + const auto number2 = ensureComparisonValue(comparisonValue); + + return before ? number < number2 : number > number2; +} + +bool RolloutEvaluator::evaluateArrayContainsAnyOf(const std::vector& array, const UserConditionComparisonValue& comparisonValue, bool negate) const { + const auto& comparisonValues = ensureComparisonValue>(comparisonValue); + + for (const auto& text : array) { + for (const auto& comparisonValue : comparisonValues) { + if (text == comparisonValue) { + return !negate; } - case NOT_ONE_OF_SENS: { - stringstream stream(comparisonValue); - string token; - auto userValueHash = (*sha1)(userValue); - bool found = false; - while (getline(stream, token, ',')) { - trim(token); - if (token == userValueHash) { - found = true; - break; - } - } - if (!found) { - logEntry << "\n" << formatMatchRule(comparisonAttribute, userValue, comparator, comparisonValue, returnValue); - return {rule.value, rule.variationId, &rule, {}, {}}; - } - break; + } + } + + return negate; +} + +bool RolloutEvaluator::evaluateSensitiveArrayContainsAnyOf(const std::vector& array, const UserConditionComparisonValue& comparisonValue, const std::string& configJsonSalt, const std::string& contextSalt, bool negate) const { + const auto& comparisonValues = ensureComparisonValue>(comparisonValue); + + for (const auto& text : array) { + const auto hash = hashComparisonValue(*sha256, text, configJsonSalt, contextSalt); + + for (const auto& comparisonValue : comparisonValues) { + if (hash == comparisonValue) { + return !negate; } - default: - continue; + } + } + + return negate; +} + +bool RolloutEvaluator::evaluatePrerequisiteFlagCondition(const PrerequisiteFlagCondition& condition, EvaluateContext& context) const { + const auto& logBuilder = context.logBuilder; + + if (logBuilder) logBuilder->appendPrerequisiteFlagCondition(condition, context.settings); + + const auto& prerequisiteFlagKey = condition.prerequisiteFlagKey; + + assert(context.settings); + const auto it = context.settings->find(prerequisiteFlagKey); + if (it == context.settings->end()) { + throw runtime_error("Prerequisite flag is missing or invalid."); + } + const auto& prerequisiteFlag = it->second; + + const auto& comparisonValue = condition.comparisonValue; + if (holds_alternative(comparisonValue)) { + throw runtime_error("Comparison value is missing or invalid."); + } + + const auto expectedSettingType = comparisonValue.getSettingType(); + if (!prerequisiteFlag.hasInvalidType() && prerequisiteFlag.type != expectedSettingType) { + string str; + throw runtime_error(string_format("Type mismatch between comparison value '%s' and prerequisite flag '%s'.", + formatSettingValue(comparisonValue, str).c_str(), prerequisiteFlagKey.c_str())); + } + + auto visitedFlags = context.getVisitedFlags(); + visitedFlags->push_back(context.key); + if (find(visitedFlags->begin(), visitedFlags->end(), prerequisiteFlagKey) != visitedFlags->end()) { + visitedFlags->push_back(prerequisiteFlagKey); + ostringstream ss; + ss << "Circular dependency detected between the following depending flags: "; + append_stringlist(ss, *visitedFlags, 0, nullopt, " -> "); + throw runtime_error(ss.str()); + } + + auto prerequisiteFlagContext = EvaluateContext::forPrerequisiteFlag(prerequisiteFlagKey, prerequisiteFlag, context); + + if (logBuilder) { + logBuilder->newLine('(') + .increaseIndent() + .newLine().appendFormat("Evaluating prerequisite flag '%s':", prerequisiteFlagKey.c_str()); + } + + const auto prerequisiteFlagEvaluateResult = evaluateSetting(prerequisiteFlagContext); + + visitedFlags->pop_back(); + + const auto prerequisiteFlagValue = *prerequisiteFlagEvaluateResult.selectedValue.value.toValueChecked(expectedSettingType); + // At this point it's ensured that the return value of the prerequisite flag is compatible with the comparison value. + + const auto comparator = condition.comparator; + + bool result; + switch (comparator) { + case PrerequisiteFlagComparator::Equals: + if (const auto& comparisonValuePtr = get_if(&comparisonValue); comparisonValuePtr) result = *comparisonValuePtr == get(prerequisiteFlagValue); + else result = *static_cast>(comparisonValue) == prerequisiteFlagValue; + break; + + case PrerequisiteFlagComparator::NotEquals: + if (const auto& comparisonValuePtr = get_if(&comparisonValue); comparisonValuePtr) result = *comparisonValuePtr != get(prerequisiteFlagValue); + else result = *static_cast>(comparisonValue) != prerequisiteFlagValue; + break; + + default: + throw runtime_error("Comparison operator is invalid."); + } + + if (logBuilder) { + string str; + logBuilder->newLine().appendFormat("Prerequisite flag evaluation result: '%s'.", formatSettingValue(prerequisiteFlagEvaluateResult.selectedValue.value, str).c_str()) + .newLine("Condition (") + .appendPrerequisiteFlagCondition(condition, context.settings) + .append(") evaluates to ").appendConditionResult(result).append('.') + .decreaseIndent() + .newLine(')'); + } + + return result; +} + +RolloutEvaluator::SuccessOrError RolloutEvaluator::evaluateSegmentCondition(const SegmentCondition& condition, EvaluateContext& context) const { + const auto& logBuilder = context.logBuilder; + + const auto& segments = context.setting.segments; + if (logBuilder) logBuilder->appendSegmentCondition(condition, segments); + + if (!context.user) { + if (!context.isMissingUserObjectLogged) { + logUserObjectIsMissing(context.key); + context.isMissingUserObjectLogged = true; } - logEntry << "\n" << formatNoMatchRule(comparisonAttribute, userValue, comparator, comparisonValue); + return string(kMissingUserObjectError); } - if (!setting.percentageItems.empty()) { - auto hashCandidate = key + user->identifier; - string hash = (*sha1)(hashCandidate).substr(0, 7); - auto num = std::stoul(hash, nullptr, 16); - auto scaled = num % 100; - double bucket = 0; - for (const auto& rolloutPercentageItem : setting.percentageItems) { - bucket += rolloutPercentageItem.percentage; - if (scaled < bucket) { - logEntry << "\n" << "Evaluating %% options. Returning " << rolloutPercentageItem.value; - return {rolloutPercentageItem.value, rolloutPercentageItem.variationId, {}, &rolloutPercentageItem, {}}; - } + const auto segmentIndex = condition.segmentIndex; + if (segmentIndex < 0 || (segments ? segments->size() : 0) <= segmentIndex) { + throw runtime_error("Segment reference is invalid."); + } + + const auto& segment = (*segments)[segmentIndex]; + + const auto& segmentName = segment.name; + if (segmentName.empty()) { + throw runtime_error("Segment name is missing."); + } + + if (logBuilder) { + logBuilder->newLine('(') + .increaseIndent() + .newLine().appendFormat("Evaluating segment '%s':", segmentName.c_str()); + } + + const auto& conditions = segment.conditions; + const auto conditionAccessor = std::function([](const UserCondition& condition) -> Condition { return condition; }); + + auto result = evaluateConditions(conditions, conditionAccessor, nullptr, segmentName, context); + SegmentComparator segmentResult; + + if (!holds_alternative(result)) { + segmentResult = get(result) ? SegmentComparator::IsIn : SegmentComparator::IsNotIn; + const auto comparator = condition.comparator; + + switch (comparator) { + case SegmentComparator::IsIn: + break; + + case SegmentComparator::IsNotIn: + result = !get(result); + break; + + default: + throw runtime_error("Comparison operator is invalid."); } + } else { + segmentResult = static_cast(-1); } - logEntry << "\n" << "Returning " << setting.value; - return {setting.value, setting.variationId, {}, {}, {}}; -} - -std::string RolloutEvaluator::formatNoMatchRule(const std::string& comparisonAttribute, - const std::string& userValue, - Comparator comparator, - const std::string& comparisonValue) { - return string_format("Evaluating rule: [%s:%s] [%s] [%s] => no match", - comparisonAttribute.c_str(), - userValue.c_str(), - comparatorToString(comparator), - comparisonValue.c_str()); -} - -std::string RolloutEvaluator::formatMatchRule(const std::string& comparisonAttribute, - const std::string& userValue, - Comparator comparator, - const std::string& comparisonValue, - const Value& returnValue) { - return string_format("Evaluating rule: [%s:%s] [%s] [%s] => match, returning: %s", - comparisonAttribute.c_str(), - userValue.c_str(), - comparatorToString(comparator), - comparisonValue.c_str(), - valueToString(returnValue).c_str()); -} - -std::string RolloutEvaluator::formatValidationErrorRule(const std::string& comparisonAttribute, - const std::string& userValue, - Comparator comparator, - const std::string& comparisonValue, - const std::string& error) { - return string_format("Evaluating rule: [%s:%s] [%s] [%s] => Skip rule, Validation error: %s", - comparisonAttribute.c_str(), - userValue.c_str(), - comparatorToString(comparator), - comparisonValue.c_str(), - error.c_str()); + if (logBuilder) { + logBuilder->newLine("Segment evaluation result: "); + + if (!holds_alternative(result)) { + logBuilder->appendFormat("User %s", getSegmentComparatorText(segmentResult)); + } else { + logBuilder->append(get(result)); + } + logBuilder->append('.'); + + logBuilder->newLine("Condition (").appendSegmentCondition(condition, segments).append(')'); + (!holds_alternative(result) + ? logBuilder->append(" evaluates to ").appendConditionResult(get(result)) + : logBuilder->append(" failed to evaluate")) + .append('.'); + + logBuilder + ->decreaseIndent() + .newLine(')'); + } + + return result; +} + +std::string RolloutEvaluator::userAttributeValueToString(const ConfigCatUser::AttributeValue& attributeValue) { + return visit([](auto&& alt) -> string { + using T = decay_t; + if constexpr (is_same_v) { + return alt; + } else if constexpr (is_same_v) { + return number_to_string(alt); + } else if constexpr (is_same_v) { + const auto unixTimeSeconds = datetime_to_unixtimeseconds(alt); + return number_to_string(unixTimeSeconds ? *unixTimeSeconds : NAN); + } else if constexpr (is_same_v>) { + nlohmann::json j = alt; + return j.dump(); + } else { + static_assert(always_false_v, "Non-exhaustive visitor."); + } + }, attributeValue); +} + +const std::string& RolloutEvaluator::getUserAttributeValueAsText(const std::string& attributeName, const ConfigCatUser::AttributeValue& attributeValue, + const UserCondition& condition, const std::string& key, std::string& text) const { + + if (const auto textPtr = get_if(&attributeValue); textPtr) { + return *textPtr; + } + + text = userAttributeValueToString(attributeValue); + logUserObjectAttributeIsAutoConverted(formatUserCondition(condition), key, attributeName, text); + return text; +} + +std::variant RolloutEvaluator::getUserAttributeValueAsSemVer(const std::string& attributeName, const ConfigCatUser::AttributeValue& attributeValue, + const UserCondition& condition, const std::string& key, semver::version& version) const { + + if (const auto textPtr = get_if(&attributeValue); textPtr) { + try { + string text(*textPtr); + trim(text); + version = semver::version::parse(text); + return &version; + } + catch (const semver::semver_exception&) { + /* intentional no-op */ + } + } + + return handleInvalidUserAttribute(condition, key, attributeName, + string_format("'%s' is not a valid semantic version", userAttributeValueToString(attributeValue).c_str())); +} + +std::variant RolloutEvaluator::getUserAttributeValueAsNumber(const std::string& attributeName, const ConfigCatUser::AttributeValue& attributeValue, + const UserCondition& condition, const std::string& key) const { + + if (const auto numberPtr = get_if(&attributeValue); numberPtr) { + return *numberPtr; + } else if (const auto textPtr = get_if(&attributeValue); textPtr) { + auto number = number_from_string(*textPtr); + if (number) return *number; + } + + return handleInvalidUserAttribute(condition, key, attributeName, + string_format("'%s' is not a valid decimal number", userAttributeValueToString(attributeValue).c_str())); +} + +std::variant RolloutEvaluator::getUserAttributeValueAsUnixTimeSeconds(const std::string& attributeName, const ConfigCatUser::AttributeValue& attributeValue, + const UserCondition& condition, const std::string& key) const { + + if (const auto dateTimePtr = get_if(&attributeValue); dateTimePtr) { + const auto unixTimeSeconds = datetime_to_unixtimeseconds(*dateTimePtr); + if (unixTimeSeconds) return *unixTimeSeconds; + } else if (const auto numberPtr = get_if(&attributeValue); numberPtr) { + return *numberPtr; + } else if (const auto textPtr = get_if(&attributeValue); textPtr) { + auto number = number_from_string(*textPtr); + if (number) return *number; + } + + return handleInvalidUserAttribute(condition, key, attributeName, + string_format("'%s' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)", userAttributeValueToString(attributeValue).c_str())); +} + +std::variant*, std::string> RolloutEvaluator::getUserAttributeValueAsStringArray(const std::string& attributeName, const ConfigCatUser::AttributeValue& attributeValue, + const UserCondition& condition, const std::string& key, std::vector& array) const { + + if (const auto arrayPtr = get_if>(&attributeValue); arrayPtr) { + return arrayPtr; + } else if (const auto textPtr = get_if(&attributeValue); textPtr) { + try { + auto j = nlohmann::json::parse(*textPtr, nullptr, true, false); + j.get_to(array); + return &array; + } + catch (...) { /* intentional no-op */ } + } + + return handleInvalidUserAttribute(condition, key, attributeName, + string_format("'%s' is not a valid string array", userAttributeValueToString(attributeValue).c_str())); +} + +void RolloutEvaluator::logUserObjectIsMissing(const std::string& key) const { + LOG_WARN(3001) + << "Cannot evaluate targeting rules and % options for setting '" << key << "' (User Object is missing). " + << "You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. " + << "Read more: https://configcat.com/docs/advanced/user-object/"; +} + +void RolloutEvaluator::logUserObjectAttributeIsMissingPercentage(const std::string& key, const std::string& attributeName) const { + LOG_WARN(3003) + << "Cannot evaluate % options for setting '" << key << "' (the User." << attributeName << " attribute is missing). " + << "You should set the User." << attributeName << " attribute in order to make targeting work properly. " + << "Read more: https://configcat.com/docs/advanced/user-object/"; +} + +void RolloutEvaluator::logUserObjectAttributeIsMissingCondition(const std::string& condition, const std::string& key, const std::string& attributeName) const { + LOG_WARN(3003) + << "Cannot evaluate condition (" << condition << ") for setting '" << key << "' (the User." << attributeName << " attribute is missing). " + << "You should set the User." << attributeName << " attribute in order to make targeting work properly. " + << "Read more: https://configcat.com/docs/advanced/user-object/"; +} + +void RolloutEvaluator::logUserObjectAttributeIsInvalid(const std::string& condition, const std::string& key, const std::string& reason, const std::string& attributeName) const { + LOG_WARN(3004) + << "Cannot evaluate condition (" << condition << ") for setting '" << key << "' (" << reason << "). " + << "Please check the User." << attributeName << " attribute and make sure that its value corresponds to the comparison operator."; +} + +void RolloutEvaluator::logUserObjectAttributeIsAutoConverted(const std::string& condition, const std::string& key, const std::string& attributeName, const std::string& attributeValue) const { + LOG_WARN(3005) + << "Evaluation of condition (" << condition << ") for setting '" << key << "' may not produce the expected result " + << "(the User." << attributeName << " attribute is not a string value, thus it was automatically converted to the string value '" << attributeValue << "'). " + << "Please make sure that using a non-string value was intended."; +} + +std::string RolloutEvaluator::handleInvalidUserAttribute(const UserCondition& condition, const std::string& key, const std::string& attributeName, const std::string& reason) const { + logUserObjectAttributeIsInvalid(formatUserCondition(condition), key, reason, attributeName); + + return string_format(kInvalidUserAttributeError, attributeName.c_str(), reason.c_str()); } } // namespace configcat diff --git a/src/rolloutevaluator.h b/src/rolloutevaluator.h index fed3b4a..7880c96 100644 --- a/src/rolloutevaluator.h +++ b/src/rolloutevaluator.h @@ -1,47 +1,142 @@ #pragma once +#include +#include +#include +#include #include #include +#include +#include +#include +#include + #include "configcat/config.h" +#include "configcat/configcatuser.h" +#include "evaluatelogbuilder.h" class SHA1; +class SHA256; namespace configcat { class ConfigCatUser; class ConfigCatLogger; +struct EvaluateContext { + const std::string& key; + const Setting& setting; + const std::shared_ptr& user; + const std::shared_ptr& settings; + + bool isMissingUserObjectLogged; + bool isMissingUserObjectAttributeLogged; + std::shared_ptr logBuilder; // initialized by RolloutEvaluator.evaluate + + EvaluateContext( + const std::string& key, + const Setting& setting, + const std::shared_ptr& user, + const std::shared_ptr& settings) + : key(key) + , setting(setting) + , user(user) + , settings(settings) + , isMissingUserObjectLogged(false) + , isMissingUserObjectAttributeLogged(false) {} + + static EvaluateContext forPrerequisiteFlag( + const std::string& key, + const Setting& setting, + EvaluateContext& dependentFlagContext) { + + EvaluateContext context(key, setting, dependentFlagContext.user, dependentFlagContext.settings); + context.visitedFlags = dependentFlagContext.getVisitedFlags(); // crucial to use `getVisitedFlags` here to make sure the list is created! + context.logBuilder = dependentFlagContext.logBuilder; + return context; + } + + inline std::shared_ptr> getVisitedFlags() { + return visitedFlags + ? visitedFlags + : (visitedFlags = std::make_shared>()); + } + +private: + std::shared_ptr> visitedFlags; +}; + +struct EvaluateResult { + const SettingValueContainer& selectedValue; + const TargetingRule* targetingRule; + const PercentageOption* percentageOption; +}; + class RolloutEvaluator { public: - RolloutEvaluator(std::shared_ptr logger); - ~RolloutEvaluator(); + RolloutEvaluator(const std::shared_ptr& logger); // Evaluate the feature flag or setting - // Returns [value, variationId, matchedEvaluationRule, matchedEvaluationPercentageRule, error] - std::tuple evaluate(const std::string& key, - const ConfigCatUser* user, - const Setting& setting); - - inline static std::string formatNoMatchRule(const std::string& comparisonAttribute, - const std::string& userValue, - Comparator comparator, - const std::string& comparisonValue); - - inline static std::string formatMatchRule(const std::string& comparisonAttribute, - const std::string& userValue, - Comparator comparator, - const std::string& comparisonValue, - const Value& returnValue); - - inline static std::string formatValidationErrorRule(const std::string& comparisonAttribute, - const std::string& userValue, - Comparator comparator, - const std::string& comparisonValue, - const std::string& error); + EvaluateResult evaluate(const std::optional& defaultValue, EvaluateContext& context, std::optional& returnValue) const; private: + using SuccessOrError = std::variant; + std::shared_ptr logger; std::unique_ptr sha1; + std::unique_ptr sha256; + + EvaluateResult evaluateSetting(EvaluateContext& context) const; + std::optional evaluateTargetingRules(const std::vector& targetingRules, EvaluateContext& context) const; + std::optional evaluatePercentageOptions(const std::vector& percentageOptions, const TargetingRule* matchedTargetingRule, EvaluateContext& context) const; + + template + RolloutEvaluator::SuccessOrError evaluateConditions(const std::vector& conditions, const std::function& conditionAccessor, + const TargetingRule* targetingRule, const std::string& contextSalt, EvaluateContext& context) const; + + RolloutEvaluator::SuccessOrError evaluateUserCondition(const UserCondition& condition, const std::string& contextSalt, EvaluateContext& context) const; + bool evaluateTextEquals(const std::string& text, const UserConditionComparisonValue& comparisonValue, bool negate) const; + bool evaluateSensitiveTextEquals(const std::string& text, const UserConditionComparisonValue& comparisonValue, const std::string& configJsonSalt, const std::string& contextSalt, bool negate) const; + bool evaluateTextIsOneOf(const std::string& text, const UserConditionComparisonValue& comparisonValue, bool negate) const; + bool evaluateSensitiveTextIsOneOf(const std::string& text, const UserConditionComparisonValue& comparisonValue, const std::string& configJsonSalt, const std::string& contextSalt, bool negate) const; + bool evaluateTextSliceEqualsAnyOf(const std::string& text, const UserConditionComparisonValue& comparisonValue, bool startsWith, bool negate) const; + bool evaluateSensitiveTextSliceEqualsAnyOf(const std::string& text, const UserConditionComparisonValue& comparisonValue, const std::string& configJsonSalt, const std::string& contextSalt, bool startsWith, bool negate) const; + bool evaluateTextContainsAnyOf(const std::string& text, const UserConditionComparisonValue& comparisonValue, bool negate) const; + bool evaluateSemVerIsOneOf(const semver::version& version, const UserConditionComparisonValue& comparisonValue, bool negate) const; + bool evaluateSemVerRelation(const semver::version& version, UserComparator comparator, const UserConditionComparisonValue& comparisonValue) const; + bool evaluateNumberRelation(double number, UserComparator comparator, const UserConditionComparisonValue& comparisonValue) const; + bool evaluateDateTimeRelation(double number, const UserConditionComparisonValue& comparisonValue, bool before) const; + bool evaluateArrayContainsAnyOf(const std::vector& array, const UserConditionComparisonValue& comparisonValue, bool negate) const; + bool evaluateSensitiveArrayContainsAnyOf(const std::vector& array, const UserConditionComparisonValue& comparisonValue, const std::string& configJsonSalt, const std::string& contextSalt, bool negate) const; + + bool evaluatePrerequisiteFlagCondition(const PrerequisiteFlagCondition& condition, EvaluateContext& context) const; + + RolloutEvaluator::SuccessOrError evaluateSegmentCondition(const SegmentCondition& condition, EvaluateContext& context) const; + + static std::string userAttributeValueToString(const ConfigCatUser::AttributeValue& attributeValue); + + const std::string& getUserAttributeValueAsText(const std::string& attributeName, const ConfigCatUser::AttributeValue& attributeValue, + const UserCondition& condition, const std::string& key, std::string& text) const; + + std::variant getUserAttributeValueAsSemVer(const std::string& attributeName, const ConfigCatUser::AttributeValue& attributeValue, + const UserCondition& condition, const std::string& key, semver::version& version) const; + + std::variant getUserAttributeValueAsNumber(const std::string& attributeName, const ConfigCatUser::AttributeValue& attributeValue, + const UserCondition& condition, const std::string& key) const; + + std::variant getUserAttributeValueAsUnixTimeSeconds(const std::string& attributeName, const ConfigCatUser::AttributeValue& attributeValue, + const UserCondition& condition, const std::string& key) const; + + std::variant*, std::string> getUserAttributeValueAsStringArray(const std::string& attributeName, const ConfigCatUser::AttributeValue& attributeValue, + const UserCondition& condition, const std::string& key, std::vector& array) const; + + void logUserObjectIsMissing(const std::string& key) const; + void logUserObjectAttributeIsMissingPercentage(const std::string& key, const std::string& attributeName) const; + void logUserObjectAttributeIsMissingCondition(const std::string& condition, const std::string& key, const std::string& attributeName) const; + void logUserObjectAttributeIsInvalid(const std::string& condition, const std::string& key, const std::string& reason, const std::string& attributeName) const; + void logUserObjectAttributeIsAutoConverted(const std::string& condition, const std::string& key, const std::string& attributeName, const std::string& attributeValue) const; + + std::string handleInvalidUserAttribute(const UserCondition& condition, const std::string& key, const std::string& attributeName, const std::string& reason) const; }; } // namespace configcat diff --git a/include/configcat/settingresult.h b/src/settingresult.h similarity index 61% rename from include/configcat/settingresult.h rename to src/settingresult.h index 288c339..ffd9e85 100644 --- a/include/configcat/settingresult.h +++ b/src/settingresult.h @@ -1,11 +1,11 @@ #pragma once -#include "config.h" +#include "configcat/config.h" namespace configcat { struct SettingResult { - const std::shared_ptr settings; + std::shared_ptr settings; double fetchTime; }; diff --git a/src/timeutils.cpp b/src/timeutils.cpp new file mode 100644 index 0000000..27cea81 --- /dev/null +++ b/src/timeutils.cpp @@ -0,0 +1,66 @@ +#include + +#include "configcat/timeutils.h" + +// Even though gmtime_s is part of the C++17 according to the reference (https://en.cppreference.com/w/c/chrono/gmtime), +// yet it's unavailable on some platforms. So, in those cases we need to "polyfill" it. This can be achieved using +// the following SFINAE trick (see also https://slashvar.github.io/2019/08/17/Detecting-Functions-existence-with-SFINAE.html): + +template +auto gmtime_impl(T tm, const time_t* time, int) -> decltype(gmtime_s(tm, time)) { + return gmtime_s(tm, time); +} + +template +struct tm* gmtime_impl(T tm, const time_t* time, float) { + auto pBuf = gmtime(time); + *tm = *pBuf; + return nullptr; +} + +using namespace std; + +namespace configcat { + +// https://howardhinnant.github.io/date_algorithms.html#days_from_civil +// From C++20, this can be replaced with std::chrono::sys_days +template constexpr Int days_from_civil(Int y, unsigned m, unsigned d) noexcept { + static_assert(std::numeric_limits::digits >= 18, "This algorithm has not been ported to a 16 bit unsigned integer"); + static_assert(std::numeric_limits::digits >= 20, "This algorithm has not been ported to a 16 bit signed integer"); + y -= m <= 2; + const Int era = (y >= 0 ? y : y-399) / 400; + const unsigned yoe = static_cast(y - era * 400); // [0, 399] + const unsigned doy = (153*(m > 2 ? m-3 : m+9) + 2)/5 + d-1; // [0, 365] + const unsigned doe = yoe * 365 + yoe/4 - yoe/100 + doy; // [0, 146096] + return era * 146097 + static_cast(doe) - 719468; +} + +std::string datetime_to_isostring(const date_time_t& tp) { + const auto secondsSinceEpoch = chrono::system_clock::to_time_t(tp); + const auto remainder = (tp - chrono::system_clock::from_time_t(secondsSinceEpoch)); + const auto milliseconds = chrono::duration_cast(remainder).count(); + + struct tm time; + gmtime_impl(&time, &secondsSinceEpoch, 0); + + string result( + sizeof "1970-01-01T00:00:00.000Z" - 1, // ctor adds the terminating NULL (see https://stackoverflow.com/a/42511919/8656352) + '\0'); + char* buf = &result[0]; + char* p = buf + strftime(buf, result.size() + 1, "%FT%T", &time); + snprintf(p, 6, ".%03dZ", static_cast(milliseconds)); + + return result; +} + +date_time_t make_datetime(int year, int month, int day, int hour, int min, int sec, int millisec) { + auto days = days_from_civil(static_cast(year), month, day); + constexpr auto dayInSeconds = 86400; + auto duration = std::chrono::seconds(static_cast(days) * dayInSeconds + sec) + + std::chrono::hours(hour) + + std::chrono::minutes(min) + + std::chrono::milliseconds(millisec); + return date_time_t{duration}; +} + +} // namespace configcat diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 0000000..f3392c9 --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,176 @@ +#include + +#include "utils.h" + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON +#define JS_NUMBER_EPSILON 2.2204460492503130808472633361816e-16 + +using namespace std; + +namespace configcat { + +std::optional datetime_to_unixtimeseconds(const std::chrono::system_clock::time_point& tp) { + long long millisecondsSinceEpoch = tp.time_since_epoch() / std::chrono::milliseconds(1); + auto timestamp = millisecondsSinceEpoch / 1000.0; + + // Allow values only between 0001-01-01T00:00:00.000Z and 9999-12-31T23:59:59.999 + return timestamp < -62135596800 || 253402300800 <= timestamp ? nullopt : optional(timestamp); +} + +std::optional datetime_from_unixtimeseconds(double timestamp) { + // Allow values only between 0001-01-01T00:00:00.000Z and 9999-12-31T23:59:59.999 + if (timestamp < -62135596800 || 253402300800 <= timestamp) { + return nullopt; + } + + auto durationSinceEpoch = std::chrono::seconds(static_cast(timestamp)) + + std::chrono::milliseconds(static_cast((timestamp - static_cast(timestamp)) * 1000)); + + return chrono::system_clock::time_point{ durationSinceEpoch }; +} + +int getExponent(double abs) { + auto exp = log10(abs); + auto ceil = std::ceil(exp); + return (int)(std::abs(exp - ceil) < JS_NUMBER_EPSILON ? ceil : floor(exp)); +} + +int getSignificantDecimals(double number) { + if (!number) { + return 0; + } + + number = std::abs(number); + auto exp = std::min(0, getExponent(number)); + + for (; exp > -17; --exp) { + auto pow10 = pow(10, exp); + auto fracr = round(number / pow10) * pow10; + if (abs(number - fracr) < number * 10.0 * JS_NUMBER_EPSILON) { + break; + } + } + + return min(17, -exp); +} + +std::string number_to_string(double number) { + if (isnan(number)) { + return "NaN"; + } + else if (isinf(number)) { + return number > 0 ? "Infinity" : "-Infinity"; + } + else if (!number) { + return "0"; + } + + const auto abs = std::abs(number); + int exp; + if (1e-6 <= abs && abs < 1e21) { + exp = 0; + } + else { + exp = getExponent(abs); + number /= pow(10, exp); + } + + // NOTE: sprintf can't really deal with 17 decimal places, + // e.g. sprintf(buf, "%.17f", 0.1) results in '0.10000000000000001'. + // So we need to manually calculate the actual number of significant decimals. + auto decimals = getSignificantDecimals(number); + + auto str = string_format(string_format("%%.%df", decimals), number); + if (exp) { + str += (exp > 0 ? "e+" : "e") + string_format("%d", exp); + } + + return str; +} + +std::optional number_from_string(const std::string& str) { + if (str.empty()) return nullopt; + + auto strPtr = const_cast(&str); + + // Make a copy of str only if necessary. + string strCopy; + if (isspace(str[0]) || isspace(str[str.size() - 1])) { + strCopy = string(str); + trim(strCopy); + if (strCopy.empty()) return nullopt; + strPtr = &strCopy; + } + + if (*strPtr == "NaN") return numeric_limits::quiet_NaN(); + else if (*strPtr == "Infinity" || *strPtr == "+Infinity") return numeric_limits::infinity(); + else if (*strPtr == "-Infinity") return -numeric_limits::infinity(); + + // Accept ',' as decimal separator. + if (strPtr->find(',') != string::npos) { + if (strPtr == &str) { + strCopy = string(str); + strPtr = &strCopy; + } + + replace(strPtr->begin(), strPtr->end(), ',', '.'); + } + + // Reject hex numbers and other forms of INF, NAN, etc. that are accepted by std::stod. + size_t index = 0; + auto ch = (*strPtr)[index]; + if (ch == '+' || ch == '-') { + ++index; + if (index >= strPtr->size()) return nullopt; + ch = (*strPtr)[index]; + } + + if (isdigit(ch)) { + ++index; + if (index < strPtr->size() && !isdigit(ch = (*strPtr)[index]) && ch != '.' && ch != 'e' && ch != 'E') return std::nullopt; + } + else if (ch != '.') return nullopt; + + size_t charsProcessed; + double value; + try { value = stod(*strPtr, &charsProcessed); } + catch (const invalid_argument&) { return nullopt; } + + // Reject strings which contain invalid characters after the number. + if (charsProcessed != strPtr->size()) { + return nullopt; + } + + return value; +} + +std::optional integer_from_string(const std::string& str) { + if (str.empty()) return nullopt; + + auto strPtr = const_cast(&str); + + // Make a copy of str only if necessary. + string strCopy; + if (isspace(str[0]) || isspace(str[str.size() - 1])) { + strCopy = string(str); + trim(strCopy); + if (strCopy.empty()) { + return nullopt; + } + strPtr = &strCopy; + } + + size_t charsProcessed; + long long value; + try { value = stoll(*strPtr, &charsProcessed); } + catch (const invalid_argument&) { return nullopt; } + + // Reject strings which contain invalid characters after the number. + if (charsProcessed != strPtr->size()) { + return nullopt; + } + + return value; +} + +} // namespace configcat diff --git a/src/utils.h b/src/utils.h index d0acf45..32c046c 100644 --- a/src/utils.h +++ b/src/utils.h @@ -1,75 +1,133 @@ #pragma once -#include #include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include + +#define STRING_FORMAT_STACKBUF_MAXSIZE 128 + +// https://stackoverflow.com/a/76675119/8656352 +template +inline constexpr bool always_false_v = false; namespace configcat { constexpr auto kEpochTime = std::chrono::system_clock::time_point(); // January 1, 1970 UTC -inline double getUtcNowSecondsSinceEpoch() { +inline double get_utcnowseconds_since_epoch() { auto duration = std::chrono::system_clock::now() - kEpochTime; return std::chrono::duration(duration).count(); } +std::optional datetime_to_unixtimeseconds(const std::chrono::system_clock::time_point& tp); +std::optional datetime_from_unixtimeseconds(double timestamp); + template -inline std::string string_format(const std::string& format, Args... args) { - int size_s = std::snprintf(nullptr, 0, format.c_str(), args...) + 1; // Extra space for '\0' - if (size_s <= 0) { +std::string string_format(const std::string& format, Args&&... args) { + std::array stackBuf; + int size_s = std::snprintf(&stackBuf[0], stackBuf.size(), format.c_str(), std::forward(args)...); + if (size_s < 0) { throw std::runtime_error("Error during string formatting."); + } else if (size_s < stackBuf.size()) { + return std::string(&stackBuf[0], &stackBuf[0] + size_s); } - auto size = static_cast(size_s); - std::unique_ptr buf(new char[size]); - std::snprintf(buf.get(), size, format.c_str(), args...); - return std::string(buf.get(), buf.get() + size - 1); // Skip '\0' inside + + std::string result( + size_s, // ctor adds the terminating NULL (see https://stackoverflow.com/a/42511919/8656352) + '\0'); + + std::snprintf(&result[0], size_s + 1, format.c_str(), std::forward(args)...); + return result; } -inline std::string str_tolower(std::string s) { - std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); }); - return s; +template +StreamType& append_stringlist( + StreamType& stream, + const std::vector& items, + size_t maxLength = 0, + const std::optional>& getOmittedItemsText = std::nullopt, + const char* separator = ", " +) { + if (!items.empty()) { + size_t i = 0; + size_t n = maxLength > 0 && items.size() > maxLength ? maxLength : items.size(); + const char* currentSeparator = ""; + + for (const auto& item : items) { + stream << currentSeparator << "'" << item << "'"; + currentSeparator = separator; + + if (++i >= n) break; + } + + if (getOmittedItemsText && n < items.size()) { + stream << (*getOmittedItemsText)(items.size() - maxLength); + } + } + return stream; } -// trim from left -inline std::string& ltrim(std::string& s, const char* t = " \t\n\r\f\v") { - s.erase(0, s.find_first_not_of(t)); - return s; +// trim from start (in place) +inline void ltrim(std::string& s) { + s.erase( + s.begin(), + std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); }) + ); } -// trim from right -inline std::string& rtrim(std::string& s, const char* t = " \t\n\r\f\v") { - s.erase(s.find_last_not_of(t) + 1); - return s; +// trim from end (in place) +inline void rtrim(std::string& s) { + s.erase( + std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), + s.end() + ); } // trim from left & right -inline std::string& trim(std::string& s, const char* t = " \t\n\r\f\v") { - return ltrim(rtrim(s, t), t); +inline void trim(std::string& s) { + rtrim(s), ltrim(s); } -inline double str_to_double(const std::string& str, bool& error) { - char* end = nullptr; // error handler for strtod - double value; - if (str.find(",") != std::string::npos) { - std::string strCopy = str; - replace(strCopy.begin(), strCopy.end(), ',', '.'); - value = strtod(strCopy.c_str(), &end); - } else { - value = strtod(str.c_str(), &end); - } - - // Handle number conversion error - error = (*end) ? true : false; - - return value; +inline bool starts_with(const std::string& str, const std::string& cmp) { + return str.rfind(cmp, 0) == 0; } -inline bool starts_with(const std::string& str, const std::string& cmp) { - return str.compare(0, cmp.length(), cmp) == 0; +inline bool ends_with(const std::string& str, const std::string& cmp) { + const auto maybe_index = str.size() - cmp.size(); + return maybe_index > 0 && (str.find(cmp, maybe_index) == maybe_index); } inline bool contains(const std::string& str, const std::string& sub) { return str.find(sub) != std::string::npos; } +inline std::string to_lower(const std::string& str) { + std::string lowerStr; + lowerStr.reserve(str.size()); + std::transform(str.begin(), str.end(), std::back_inserter(lowerStr), [](unsigned char c) { return std::tolower(c); }); + return lowerStr; +} + +template +inline typename Type::const_iterator find_caseinsensitive(const Type& map, const std::string& searchKey) { + auto lowerSearchKey = to_lower(searchKey); + return std::find_if(map.begin(), map.end(), [&lowerSearchKey](const auto& pair) { + return to_lower(pair.first) == lowerSearchKey; + }); +} + +std::string number_to_string(double number); + +std::optional number_from_string(const std::string& str); + +std::optional integer_from_string(const std::string& str); + } // namespace configcat diff --git a/src/version.h b/src/version.h index a788fe3..e782281 100644 --- a/src/version.h +++ b/src/version.h @@ -1,3 +1,3 @@ #pragma once -#define CONFIGCAT_VERSION "3.2.0" +#define CONFIGCAT_VERSION "4.0.0" diff --git a/test/data/comparison_attribute_conversion.json b/test/data/comparison_attribute_conversion.json new file mode 100644 index 0000000..5a900ae --- /dev/null +++ b/test/data/comparison_attribute_conversion.json @@ -0,0 +1,789 @@ +{ + "p": { + "u": "https://test-cdn-global.configcat.com", + "r": 0, + "s": "uM29sy1rjx71ze3ehr\u002BqCnoIpx8NZgL8V//MN7OL1aM=" + }, + "f": { + "numberToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "0.12345" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionInt": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "125" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionPositiveExp": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-1.23456789e+96" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNegativeExp": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-1.23456789e-96" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNaN": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "NaN" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionPositiveInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "numberToStringConversionNegativeInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "1680307199.999" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionNaN": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "NaN" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionPositiveInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "dateToStringConversionNegativeInf": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "-Infinity" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversion": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"read\",\"Write\",\" eXecute \"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionEmpty": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionSpecialChars": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"+<>%\\\"'\\\\/\\t\\r\\n\"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + }, + "stringArrayToStringConversionUnicode": { + "t": 1, + "a": "Custom1", + "r": [ + { + "c": [ + { + "u": { + "a": "Custom1", + "c": 28, + "s": "[\"äöüÄÖÜçéèñışğ⢙✓😀\"]" + } + } + ], + "p": [ + { + "p": 20, + "v": { + "s": "1" + } + }, + { + "p": 20, + "v": { + "s": "2" + } + }, + { + "p": 20, + "v": { + "s": "3" + } + }, + { + "p": 20, + "v": { + "s": "4" + } + }, + { + "p": 20, + "v": { + "s": "5" + } + } + ] + } + ], + "v": { + "s": "0" + } + } + } +} diff --git a/test/data/comparison_attribute_trimming.json b/test/data/comparison_attribute_trimming.json new file mode 100644 index 0000000..a42df5f --- /dev/null +++ b/test/data/comparison_attribute_trimming.json @@ -0,0 +1,985 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "VjBfGYcmyHzLBv5EINgSBbX6/rYevYGWQhF3Zk5t8i4=" + }, + "f": { + "arraycontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 34, + "l": [ + "USA" + ] + } + } + ], + "s": { + "v": { + "s": "34 trim" + }, + "i": "99c90883" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "9c66d87c" + }, + "arraycontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 26, + "l": [ + "09d5761537a8136eb7fc45a53917b51cb9dcd2bb9b62ffa24ace0e8a7600a3c7" + ] + } + } + ], + "s": { + "v": { + "s": "26 trim" + }, + "i": "706c94b6" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "3b342be3" + }, + "arraynotcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 35, + "l": [ + "USA" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4eeb2176" + } + } + ], + "v": { + "s": "35 trim" + }, + "i": "98bc8ebb" + }, + "arraynotcontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 27, + "l": [ + "99d06b6b3669b906803c285267f76fe4e2ccc194b00801ab07f2fd49939b6960" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "8f248790" + } + } + ], + "v": { + "s": "27 trim" + }, + "i": "278ddbe9" + }, + "endswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 32, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "32 trim" + }, + "i": "0ac9e321" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "777456df" + }, + "endswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 24, + "l": [ + "5_7eb158c29b48b62cec860dffc459171edbfeef458bcc8e8bb62956d823eef3df" + ] + } + } + ], + "s": { + "v": { + "s": "24 trim" + }, + "i": "0364bf98" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2f6fc77b" + }, + "equals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 28, + "s": "12345" + } + } + ], + "s": { + "v": { + "s": "28 trim" + }, + "i": "f2a682ca" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "0f806923" + }, + "equalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 20, + "s": "ea0d05859bb737105eea40bc605f6afd542c8f50f8497cd21ace38e731d7eef0" + } + } + ], + "s": { + "v": { + "s": "20 trim" + }, + "i": "6f1798e9" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "771ecd4d" + }, + "isnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "79d49e05" + } + } + ], + "v": { + "s": "1 trim" + }, + "i": "61d13448" + }, + "isnotoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "1c2df623" + } + } + ], + "v": { + "s": "17 trim" + }, + "i": "0bc3daa1" + }, + "isoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 0, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "0 trim" + }, + "i": "308f0749" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "90984858" + }, + "isoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 16, + "l": [ + "1765b470044971bbc19e7bed10112199c5da9c626455f86be109fef96e747911" + ] + } + } + ], + "s": { + "v": { + "s": "16 trim" + }, + "i": "cd78a85d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "30b9483f" + }, + "notendswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 33, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "b0d7203e" + } + } + ], + "v": { + "s": "33 trim" + }, + "i": "89740c7e" + }, + "notendswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 25, + "l": [ + "5_2a338d3beb8ebe2e711d198420d04e2627e39501c2fcc7d5b3b8d93540691097" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "059f59e3" + } + } + ], + "v": { + "s": "25 trim" + }, + "i": "c1e95c48" + }, + "notequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 29, + "s": "12345" + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "af1f1e95" + } + } + ], + "v": { + "s": "29 trim" + }, + "i": "219e6bac" + }, + "notequalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 21, + "s": "650fe0e8e86030b5f73ccd77e6532f307adf82506048a22f02d95386206ecea1" + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "9fe2b26b" + } + } + ], + "v": { + "s": "21 trim" + }, + "i": "9211e9f1" + }, + "notstartwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 31, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "ebe3ed2d" + } + } + ], + "v": { + "s": "31 trim" + }, + "i": "7deb7219" + }, + "notstartwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 23, + "l": [ + "5_586ab2ec61946cb1457d4af170d88e7f14e655d9debf352b4ab6bf5bf77df3f7" + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "7b606e54" + } + } + ], + "v": { + "s": "23 trim" + }, + "i": "edec740e" + }, + "semvergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 8, + "s": "0.1.1" + } + } + ], + "s": { + "v": { + "s": "8 trim" + }, + "i": "25edfdc1" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "cb0224fd" + }, + "semvergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 9, + "s": "0.1.1" + } + } + ], + "s": { + "v": { + "s": "9 trim" + }, + "i": "d8960b43" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "530ea45c" + }, + "semverisnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 5, + "l": [ + "1.0.1" + ] + } + } + ], + "s": { + "v": { + "s": "5 trim" + }, + "i": "cb1bad57" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "4a7025a4" + }, + "semverisoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 4, + "l": [ + "1.0.0" + ] + } + } + ], + "s": { + "v": { + "s": "4 trim" + }, + "i": "6cc37494" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "842a56b5" + }, + "semverless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 6, + "s": "1.0.1" + } + } + ], + "s": { + "v": { + "s": "6 trim" + }, + "i": "64c04b67" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ae58de40" + }, + "semverlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 7, + "s": "1.0.1" + } + } + ], + "s": { + "v": { + "s": "7 trim" + }, + "i": "7c62748d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "631a1888" + }, + "startwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 30, + "l": [ + "12345" + ] + } + } + ], + "s": { + "v": { + "s": "30 trim" + }, + "i": "475a9c4f" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "5a73105a" + }, + "startwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 22, + "l": [ + "5_67a323069ee45fef4ccd8365007d4713f7a3bc87764943b1139e8e50d1aee8fd" + ] + } + } + ], + "s": { + "v": { + "s": "22 trim" + }, + "i": "7650175d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "a38edbee" + }, + "dateafter": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Date", + "c": 19, + "d": 1705251600 + } + } + ], + "s": { + "v": { + "s": "19 trim" + }, + "i": "83e580ce" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "1c12e0cc" + }, + "datebefore": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Date", + "c": 18, + "d": 1705255200 + } + } + ], + "s": { + "v": { + "s": "18 trim" + }, + "i": "34614b07" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "26d4f328" + }, + "numberequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 10, + "d": 3 + } + } + ], + "s": { + "v": { + "s": "10 trim" + }, + "i": "6a8c0a08" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "7b8e49b9" + }, + "numbergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 14, + "d": 2 + } + } + ], + "s": { + "v": { + "s": "14 trim" + }, + "i": "2037a7a4" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "902f9bd9" + }, + "numbergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 15, + "d": 2 + } + } + ], + "s": { + "v": { + "s": "15 trim" + }, + "i": "527c49d2" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2280c961" + }, + "numberless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 12, + "d": 4 + } + } + ], + "s": { + "v": { + "s": "12 trim" + }, + "i": "c454f775" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ec935943" + }, + "numberlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 13, + "d": 4 + } + } + ], + "s": { + "v": { + "s": "13 trim" + }, + "i": "1e31aed8" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "1d53c679" + }, + "numbernotequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Number", + "c": 11, + "d": 6 + } + } + ], + "s": { + "v": { + "s": "11 trim" + }, + "i": "e8d7cf05" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "21c749a7" + }, + "containsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "f750380a" + } + } + ], + "v": { + "s": "2 trim" + }, + "i": "c3ab37cf" + }, + "notcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 3, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "3 trim" + }, + "i": "4b8760c4" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "f91ecf16" + } + } +} \ No newline at end of file diff --git a/test/data/comparison_value_trimming.json b/test/data/comparison_value_trimming.json new file mode 100644 index 0000000..db91703 --- /dev/null +++ b/test/data/comparison_value_trimming.json @@ -0,0 +1,777 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "zsVN1DQ9Oa2FjFc96MvPfMM5Vs+KKV00NyybJZipyf4=" + }, + "f": { + "arraycontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 34, + "l": [ + " USA " + ] + } + } + ], + "s": { + "v": { + "s": "34 trim" + }, + "i": "99c90883" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "9c66d87c" + }, + "arraycontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 26, + "l": [ + " 028fdb841bf3b2cc27fce407da08f87acd3a58a08c67d819cdb9351857b14237 " + ] + } + } + ], + "s": { + "v": { + "s": "26 trim" + }, + "i": "706c94b6" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "3b342be3" + }, + "arraynotcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 35, + "l": [ + " USA " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4eeb2176" + } + } + ], + "v": { + "s": "35 trim" + }, + "i": "98bc8ebb" + }, + "arraynotcontainsanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Country", + "c": 27, + "l": [ + " 60b747c290642863f9a6c68773ed309a9fb02c6c1ae65c77037046918f4c1d3c " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "8f248790" + } + } + ], + "v": { + "s": "27 trim" + }, + "i": "278ddbe9" + }, + "containsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "2 trim" + }, + "i": "f750380a" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "c3ab37cf" + }, + "endswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 32, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "32 trim" + }, + "i": "0ac9e321" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "777456df" + }, + "endswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 24, + "l": [ + " 5_a6ce5e2838d4e0c27cd705c90f39e60d79056062983c39951668cf947ec406c2 " + ] + } + } + ], + "s": { + "v": { + "s": "24 trim" + }, + "i": "0364bf98" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "2f6fc77b" + }, + "equals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 28, + "s": " 12345 " + } + } + ], + "s": { + "v": { + "s": "28 trim" + }, + "i": "f2a682ca" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "0f806923" + }, + "equalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 20, + "s": " a2868640b1fe24c98e50b168756d83fd03779dd4349d6ddab5d7d6ef8dad13bd " + } + } + ], + "s": { + "v": { + "s": "20 trim" + }, + "i": "6f1798e9" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "771ecd4d" + }, + "isnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "79d49e05" + } + } + ], + "v": { + "s": "1 trim" + }, + "i": "61d13448" + }, + "isnotoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 1, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "1c2df623" + } + } + ], + "v": { + "s": "17 trim" + }, + "i": "0bc3daa1" + }, + "isoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 0, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "0 trim" + }, + "i": "308f0749" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "90984858" + }, + "isoneofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 16, + "l": [ + " 55ce90920d20fc0bf8078471062a85f82cc5ea2226012a901a5045775bace0f4 " + ] + } + } + ], + "s": { + "v": { + "s": "16 trim" + }, + "i": "cd78a85d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "30b9483f" + }, + "notcontainsanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 3, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "4b8760c4" + } + } + ], + "v": { + "s": "3 trim" + }, + "i": "f91ecf16" + }, + "notendswithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 33, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "b0d7203e" + } + } + ], + "v": { + "s": "33 trim" + }, + "i": "89740c7e" + }, + "notendswithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 25, + "l": [ + " 5_c517fc957907e30b6a790540a20172a3a5d3a7458a85e340a7b1a1ac982be278 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "059f59e3" + } + } + ], + "v": { + "s": "25 trim" + }, + "i": "c1e95c48" + }, + "notequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 29, + "s": " 12345 " + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "af1f1e95" + } + } + ], + "v": { + "s": "29 trim" + }, + "i": "219e6bac" + }, + "notequalshashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 21, + "s": " 31ceae14b865b0842e93fdc3a42a7e45780ccc41772ca9355db50e09d81e13ef " + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "9fe2b26b" + } + } + ], + "v": { + "s": "21 trim" + }, + "i": "9211e9f1" + }, + "notstartwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 31, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "ebe3ed2d" + } + } + ], + "v": { + "s": "31 trim" + }, + "i": "7deb7219" + }, + "notstartwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 23, + "l": [ + " 5_3643bbdd1bce4021fe4dbd55e6cc2f4902e4f50e592597d1a2d0e944fb7dfb42 " + ] + } + } + ], + "s": { + "v": { + "s": "no trim" + }, + "i": "7b606e54" + } + } + ], + "v": { + "s": "23 trim" + }, + "i": "edec740e" + }, + "semvergreater": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 8, + "s": " 0.1.1 " + } + } + ], + "s": { + "v": { + "s": "8 trim" + }, + "i": "25edfdc1" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "cb0224fd" + }, + "semvergreaterequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 9, + "s": " 0.1.1 " + } + } + ], + "s": { + "v": { + "s": "9 trim" + }, + "i": "d8960b43" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "530ea45c" + }, + "semverisnotoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 5, + "l": [ + " 1.0.1 " + ] + } + } + ], + "s": { + "v": { + "s": "5 trim" + }, + "i": "cb1bad57" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "4a7025a4" + }, + "semverisoneof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 4, + "l": [ + " 1.0.0 " + ] + } + } + ], + "s": { + "v": { + "s": "4 trim" + }, + "i": "6cc37494" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "842a56b5" + }, + "semverless": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 6, + "s": " 1.0.1 " + } + } + ], + "s": { + "v": { + "s": "6 trim" + }, + "i": "64c04b67" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "ae58de40" + }, + "semverlessequals": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Version", + "c": 7, + "s": " 1.0.1 " + } + } + ], + "s": { + "v": { + "s": "7 trim" + }, + "i": "7c62748d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "631a1888" + }, + "startwithanyof": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 30, + "l": [ + " 12345 " + ] + } + } + ], + "s": { + "v": { + "s": "30 trim" + }, + "i": "475a9c4f" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "5a73105a" + }, + "startwithanyofhashed": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 22, + "l": [ + " 5_3e052709552ca9d5bd6c459cb7ab0389f3210f6aafc3d006a2481635e9614a7c " + ] + } + } + ], + "s": { + "v": { + "s": "22 trim" + }, + "i": "7650175d" + } + } + ], + "v": { + "s": "no trim" + }, + "i": "a38edbee" + } + } +} \ No newline at end of file diff --git a/test/data/evaluation/1_targeting_rule.json b/test/data/evaluation/1_targeting_rule.json new file mode 100644 index 0000000..596bd2b --- /dev/null +++ b/test/data/evaluation/1_targeting_rule.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "1_rule_no_user.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "1_rule_no_targeted_attribute.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": "Cat", + "expectedLog": "1_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "stringContainsDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": "Dog", + "expectedLog": "1_rule_matching_targeted_attribute.txt" + } + ] +} diff --git a/test/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt b/test/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt new file mode 100644 index 0000000..f05c6f6 --- /dev/null +++ b/test/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/test/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt b/test/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..80702e9 --- /dev/null +++ b/test/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt @@ -0,0 +1,6 @@ +WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/test/data/evaluation/1_targeting_rule/1_rule_no_user.txt b/test/data/evaluation/1_targeting_rule/1_rule_no_user.txt new file mode 100644 index 0000000..20e290f --- /dev/null +++ b/test/data/evaluation/1_targeting_rule/1_rule_no_user.txt @@ -0,0 +1,6 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/test/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt b/test/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..49d1252 --- /dev/null +++ b/test/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => no match + Returning 'Cat'. diff --git a/test/data/evaluation/2_targeting_rules.json b/test/data/evaluation/2_targeting_rules.json new file mode 100644 index 0000000..5cf8a3c --- /dev/null +++ b/test/data/evaluation/2_targeting_rules.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "2_rules_no_user.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "2_rules_no_targeted_attribute.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Custom1": "user" + }, + "returnValue": "Cat", + "expectedLog": "2_rules_not_matching_targeted_attribute.txt" + }, + { + "key": "stringIsInDogDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Custom1": "admin" + }, + "returnValue": "Dog", + "expectedLog": "2_rules_matching_targeted_attribute.txt" + } + ] +} diff --git a/test/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt b/test/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt new file mode 100644 index 0000000..d124a4f --- /dev/null +++ b/test/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"admin"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/test/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt b/test/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt new file mode 100644 index 0000000..0e02076 --- /dev/null +++ b/test/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt @@ -0,0 +1,9 @@ +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3003] Cannot evaluate condition (User.Custom1 IS ONE OF ['admin']) for setting 'stringIsInDogDefaultCat' (the User.Custom1 attribute is missing). You should set the User.Custom1 attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/test/data/evaluation/2_targeting_rules/2_rules_no_user.txt b/test/data/evaluation/2_targeting_rules/2_rules_no_user.txt new file mode 100644 index 0000000..9674ea8 --- /dev/null +++ b/test/data/evaluation/2_targeting_rules/2_rules_no_user.txt @@ -0,0 +1,8 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/test/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt b/test/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..72217b2 --- /dev/null +++ b/test/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"user"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => no match + Returning 'Cat'. diff --git a/test/data/evaluation/and_rules.json b/test/data/evaluation/and_rules.json new file mode 100644 index 0000000..c6ed879 --- /dev/null +++ b/test/data/evaluation/and_rules.json @@ -0,0 +1,22 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", + "tests": [ + { + "key": "emailAnd", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "and_rules_no_user.txt" + }, + { + "key": "emailAnd", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "jane@configcat.com" + }, + "returnValue": "Cat", + "expectedLog": "and_rules_user.txt" + } + ] +} diff --git a/test/data/evaluation/and_rules/and_rules_no_user.txt b/test/data/evaluation/and_rules/and_rules_no_user.txt new file mode 100644 index 0000000..0b8fb25 --- /dev/null +++ b/test/data/evaluation/and_rules/and_rules_no_user.txt @@ -0,0 +1,7 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'emailAnd' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'emailAnd' + Evaluating targeting rules and applying the first match if any: + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/test/data/evaluation/and_rules/and_rules_user.txt b/test/data/evaluation/and_rules/and_rules_user.txt new file mode 100644 index 0000000..92c59ce --- /dev/null +++ b/test/data/evaluation/and_rules/and_rules_user.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'emailAnd' for User '{"Identifier":"12345","Email":"jane@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true + AND User.Email CONTAINS ANY OF ['@'] => true + AND User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'Dog' => no match + Returning 'Cat'. diff --git a/test/data/evaluation/comparators.json b/test/data/evaluation/comparators.json new file mode 100644 index 0000000..5d5631e --- /dev/null +++ b/test/data/evaluation/comparators.json @@ -0,0 +1,20 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", + "tests": [ + { + "key": "allinone", + "defaultValue": "", + "user": { + "Identifier": "12345", + "Email": "joe@example.com", + "Country": "[\"USA\"]", + "Version": "1.0.0", + "Number": "1.0", + "Date": "1693497500" + }, + "returnValue": "default", + "expectedLog": "allinone.txt" + } + ] +} diff --git a/test/data/evaluation/comparators/allinone.txt b/test/data/evaluation/comparators/allinone.txt new file mode 100644 index 0000000..18b163c --- /dev/null +++ b/test/data/evaluation/comparators/allinone.txt @@ -0,0 +1,57 @@ +INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@example.com","Country":"[\"USA\"]","Date":"1693497500","Number":"1.0","Version":"1.0.0"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email EQUALS '' => true + AND User.Email NOT EQUALS '' => false, skipping the remaining AND conditions + THEN '1h' => no match + - IF User.Email EQUALS 'joe@example.com' => true + AND User.Email NOT EQUALS 'joe@example.com' => false, skipping the remaining AND conditions + THEN '1c' => no match + - IF User.Email IS ONE OF [<1 hashed value>] => true + AND User.Email IS NOT ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '2h' => no match + - IF User.Email IS ONE OF ['joe@example.com'] => true + AND User.Email IS NOT ONE OF ['joe@example.com'] => false, skipping the remaining AND conditions + THEN '2c' => no match + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true + AND User.Email NOT STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '3h' => no match + - IF User.Email STARTS WITH ANY OF ['joe@'] => true + AND User.Email NOT STARTS WITH ANY OF ['joe@'] => false, skipping the remaining AND conditions + THEN '3c' => no match + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => true + AND User.Email NOT ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '4h' => no match + - IF User.Email ENDS WITH ANY OF ['@example.com'] => true + AND User.Email NOT ENDS WITH ANY OF ['@example.com'] => false, skipping the remaining AND conditions + THEN '4c' => no match + - IF User.Email CONTAINS ANY OF ['e@e'] => true + AND User.Email NOT CONTAINS ANY OF ['e@e'] => false, skipping the remaining AND conditions + THEN '5' => no match + - IF User.Version IS ONE OF ['1.0.0'] => true + AND User.Version IS NOT ONE OF ['1.0.0'] => false, skipping the remaining AND conditions + THEN '6' => no match + - IF User.Version < '1.0.1' => true + AND User.Version >= '1.0.1' => false, skipping the remaining AND conditions + THEN '7' => no match + - IF User.Version > '0.9.9' => true + AND User.Version <= '0.9.9' => false, skipping the remaining AND conditions + THEN '8' => no match + - IF User.Number = '1' => true + AND User.Number != '1' => false, skipping the remaining AND conditions + THEN '9' => no match + - IF User.Number < '1.1' => true + AND User.Number >= '1.1' => false, skipping the remaining AND conditions + THEN '10' => no match + - IF User.Number > '0.9' => true + AND User.Number <= '0.9' => false, skipping the remaining AND conditions + THEN '11' => no match + - IF User.Date BEFORE '1693497600' (2023-08-31T16:00:00.000Z UTC) => true + AND User.Date AFTER '1693497600' (2023-08-31T16:00:00.000Z UTC) => false, skipping the remaining AND conditions + THEN '12' => no match + - IF User.Country ARRAY CONTAINS ANY OF [<1 hashed value>] => true + AND User.Country ARRAY NOT CONTAINS ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '13h' => no match + - IF User.Country ARRAY CONTAINS ANY OF ['USA'] => true + AND User.Country ARRAY NOT CONTAINS ANY OF ['USA'] => false, skipping the remaining AND conditions + THEN '13c' => no match + Returning 'default'. diff --git a/test/data/evaluation/comparators/allinone.txt.linux b/test/data/evaluation/comparators/allinone.txt.linux new file mode 100644 index 0000000..84e9b32 --- /dev/null +++ b/test/data/evaluation/comparators/allinone.txt.linux @@ -0,0 +1,57 @@ +INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@example.com","Country":"[\"USA\"]","Version":"1.0.0","Number":"1.0","Date":"1693497500"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email EQUALS '' => true + AND User.Email NOT EQUALS '' => false, skipping the remaining AND conditions + THEN '1h' => no match + - IF User.Email EQUALS 'joe@example.com' => true + AND User.Email NOT EQUALS 'joe@example.com' => false, skipping the remaining AND conditions + THEN '1c' => no match + - IF User.Email IS ONE OF [<1 hashed value>] => true + AND User.Email IS NOT ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '2h' => no match + - IF User.Email IS ONE OF ['joe@example.com'] => true + AND User.Email IS NOT ONE OF ['joe@example.com'] => false, skipping the remaining AND conditions + THEN '2c' => no match + - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true + AND User.Email NOT STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '3h' => no match + - IF User.Email STARTS WITH ANY OF ['joe@'] => true + AND User.Email NOT STARTS WITH ANY OF ['joe@'] => false, skipping the remaining AND conditions + THEN '3c' => no match + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => true + AND User.Email NOT ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '4h' => no match + - IF User.Email ENDS WITH ANY OF ['@example.com'] => true + AND User.Email NOT ENDS WITH ANY OF ['@example.com'] => false, skipping the remaining AND conditions + THEN '4c' => no match + - IF User.Email CONTAINS ANY OF ['e@e'] => true + AND User.Email NOT CONTAINS ANY OF ['e@e'] => false, skipping the remaining AND conditions + THEN '5' => no match + - IF User.Version IS ONE OF ['1.0.0'] => true + AND User.Version IS NOT ONE OF ['1.0.0'] => false, skipping the remaining AND conditions + THEN '6' => no match + - IF User.Version < '1.0.1' => true + AND User.Version >= '1.0.1' => false, skipping the remaining AND conditions + THEN '7' => no match + - IF User.Version > '0.9.9' => true + AND User.Version <= '0.9.9' => false, skipping the remaining AND conditions + THEN '8' => no match + - IF User.Number = '1' => true + AND User.Number != '1' => false, skipping the remaining AND conditions + THEN '9' => no match + - IF User.Number < '1.1' => true + AND User.Number >= '1.1' => false, skipping the remaining AND conditions + THEN '10' => no match + - IF User.Number > '0.9' => true + AND User.Number <= '0.9' => false, skipping the remaining AND conditions + THEN '11' => no match + - IF User.Date BEFORE '1693497600' (2023-08-31T16:00:00.000Z UTC) => true + AND User.Date AFTER '1693497600' (2023-08-31T16:00:00.000Z UTC) => false, skipping the remaining AND conditions + THEN '12' => no match + - IF User.Country ARRAY CONTAINS ANY OF [<1 hashed value>] => true + AND User.Country ARRAY NOT CONTAINS ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN '13h' => no match + - IF User.Country ARRAY CONTAINS ANY OF ['USA'] => true + AND User.Country ARRAY NOT CONTAINS ANY OF ['USA'] => false, skipping the remaining AND conditions + THEN '13c' => no match + Returning 'default'. diff --git a/test/data/evaluation/epoch_date_validation.json b/test/data/evaluation/epoch_date_validation.json new file mode 100644 index 0000000..e916d21 --- /dev/null +++ b/test/data/evaluation/epoch_date_validation.json @@ -0,0 +1,16 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", + "tests": [ + { + "key": "boolTrueIn202304", + "defaultValue": true, + "returnValue": false, + "expectedLog": "date_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "2023.04.10" + } + } + ] +} diff --git a/test/data/evaluation/epoch_date_validation/date_error.txt b/test/data/evaluation/epoch_date_validation/date_error.txt new file mode 100644 index 0000000..610b8f5 --- /dev/null +++ b/test/data/evaluation/epoch_date_validation/date_error.txt @@ -0,0 +1,7 @@ +WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC)) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +INFO [5000] Evaluating 'boolTrueIn202304' for User '{"Identifier":"12345","Custom1":"2023.04.10"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC) => false, skipping the remaining AND conditions + THEN 'true' => cannot evaluate, the User.Custom1 attribute is invalid ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/test/data/evaluation/list_truncation.json b/test/data/evaluation/list_truncation.json new file mode 100644 index 0000000..64e9426 --- /dev/null +++ b/test/data/evaluation/list_truncation.json @@ -0,0 +1,14 @@ +{ + "jsonOverride": "test_list_truncation.json", + "tests": [ + { + "key": "booleanKey1", + "defaultValue": false, + "user": { + "Identifier": "12" + }, + "returnValue": true, + "expectedLog": "list_truncation.txt" + } + ] +} diff --git a/test/data/evaluation/list_truncation/list_truncation.txt b/test/data/evaluation/list_truncation/list_truncation.txt new file mode 100644 index 0000000..10a0195 --- /dev/null +++ b/test/data/evaluation/list_truncation/list_truncation.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'booleanKey1' for User '{"Identifier":"12"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true + AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <1 more value>] => true + AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <2 more values>] => true + THEN 'true' => MATCH, applying rule + Returning 'true'. diff --git a/test/data/evaluation/list_truncation/test_list_truncation.json b/test/data/evaluation/list_truncation/test_list_truncation.json new file mode 100644 index 0000000..6fdde45 --- /dev/null +++ b/test/data/evaluation/list_truncation/test_list_truncation.json @@ -0,0 +1,83 @@ +{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0, + "s": "test-salt" + }, + "f": { + "booleanKey1": { + "t": 0, + "v": { + "b": false + }, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11" + ] + } + }, + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12" + ] + } + } + ], + "s": { + "v": { + "b": true + } + } + } + ] + } + } +} diff --git a/test/data/evaluation/number_validation.json b/test/data/evaluation/number_validation.json new file mode 100644 index 0000000..640cf3d --- /dev/null +++ b/test/data/evaluation/number_validation.json @@ -0,0 +1,16 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", + "tests": [ + { + "key": "number", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "number_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "not_a_number" + } + } + ] +} diff --git a/test/data/evaluation/number_validation/number_error.txt b/test/data/evaluation/number_validation/number_error.txt new file mode 100644 index 0000000..f936809 --- /dev/null +++ b/test/data/evaluation/number_validation/number_error.txt @@ -0,0 +1,6 @@ +WARNING [3004] Cannot evaluate condition (User.Custom1 != '5') for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +INFO [5000] Evaluating 'number' for User '{"Identifier":"12345","Custom1":"not_a_number"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 != '5' THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. diff --git a/test/data/evaluation/options_after_targeting_rule.json b/test/data/evaluation/options_after_targeting_rule.json new file mode 100644 index 0000000..803840e --- /dev/null +++ b/test/data/evaluation/options_after_targeting_rule.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "returnValue": -1, + "expectedLog": "options_after_targeting_rule_no_user.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345" + }, + "returnValue": 2, + "expectedLog": "options_after_targeting_rule_no_targeted_attribute.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": 2, + "expectedLog": "options_after_targeting_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "integer25One25Two25Three25FourAdvancedRules", + "defaultValue": 42, + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": 5, + "expectedLog": "options_after_targeting_rule_matching_targeted_attribute.txt" + } + ] +} diff --git a/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt b/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt new file mode 100644 index 0000000..6815fa3 --- /dev/null +++ b/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => MATCH, applying rule + Returning '5'. diff --git a/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt b/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..8e6facb --- /dev/null +++ b/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt @@ -0,0 +1,9 @@ +WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'integer25One25Two25Three25FourAdvancedRules' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) + - Hash value 25 selects % option 2 (25%), '2'. + Returning '2'. diff --git a/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt b/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt new file mode 100644 index 0000000..45f6354 --- /dev/null +++ b/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt @@ -0,0 +1,7 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'integer25One25Two25Three25FourAdvancedRules' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Skipping % options because the User Object is missing. + Returning '-1'. diff --git a/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt b/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..c412e5a --- /dev/null +++ b/test/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => no match + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) + - Hash value 25 selects % option 2 (25%), '2'. + Returning '2'. diff --git a/test/data/evaluation/options_based_on_custom_attr.json b/test/data/evaluation/options_based_on_custom_attr.json new file mode 100644 index 0000000..5f8d1c6 --- /dev/null +++ b/test/data/evaluation/options_based_on_custom_attr.json @@ -0,0 +1,31 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", + "tests": [ + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "options_custom_attribute_no_user.txt" + }, + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Chicken", + "expectedLog": "no_options_custom_attribute.txt" + }, + { + "key": "string75Cat0Dog25Falcon0HorseCustomAttr", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Country": "US" + }, + "returnValue": "Cat", + "expectedLog": "matching_options_custom_attribute.txt" + } + ] +} diff --git a/test/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt b/test/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt new file mode 100644 index 0000000..2621086 --- /dev/null +++ b/test/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt @@ -0,0 +1,5 @@ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345","Country":"US"}' + Evaluating % options based on the User.Country attribute: + - Computing hash in the [0..99] range from User.Country => 70 (this value is sticky and consistent across all SDKs) + - Hash value 70 selects % option 1 (75%), 'Cat'. + Returning 'Cat'. diff --git a/test/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt b/test/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt new file mode 100644 index 0000000..c92c5bc --- /dev/null +++ b/test/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt @@ -0,0 +1,4 @@ +WARNING [3003] Cannot evaluate % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345"}' + Skipping % options because the User.Country attribute is missing. + Returning 'Chicken'. diff --git a/test/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt b/test/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt new file mode 100644 index 0000000..9b6ec39 --- /dev/null +++ b/test/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt @@ -0,0 +1,4 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' + Skipping % options because the User Object is missing. + Returning 'Chicken'. diff --git a/test/data/evaluation/options_based_on_user_id.json b/test/data/evaluation/options_based_on_user_id.json new file mode 100644 index 0000000..442f575 --- /dev/null +++ b/test/data/evaluation/options_based_on_user_id.json @@ -0,0 +1,21 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "string75Cat0Dog25Falcon0Horse", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "options_user_attribute_no_user.txt" + }, + { + "key": "string75Cat0Dog25Falcon0Horse", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "options_user_attribute_user.txt" + } + ] +} diff --git a/test/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt b/test/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt new file mode 100644 index 0000000..7d3116e --- /dev/null +++ b/test/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt @@ -0,0 +1,4 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0Horse' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' + Skipping % options because the User Object is missing. + Returning 'Chicken'. diff --git a/test/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt b/test/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt new file mode 100644 index 0000000..dac8dd6 --- /dev/null +++ b/test/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt @@ -0,0 +1,5 @@ +INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' for User '{"Identifier":"12345"}' + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 21 (this value is sticky and consistent across all SDKs) + - Hash value 21 selects % option 1 (75%), 'Cat'. + Returning 'Cat'. diff --git a/test/data/evaluation/options_within_targeting_rule.json b/test/data/evaluation/options_within_targeting_rule.json new file mode 100644 index 0000000..4c6c533 --- /dev/null +++ b/test/data/evaluation/options_within_targeting_rule.json @@ -0,0 +1,52 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", + "tests": [ + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_no_user.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_no_targeted_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@example.com" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_not_matching_targeted_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt" + }, + { + "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "joe@configcat.com", + "Country": "US" + }, + "returnValue": "Cat", + "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt" + } + ] +} diff --git a/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt b/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt new file mode 100644 index 0000000..db721f5 --- /dev/null +++ b/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt @@ -0,0 +1,7 @@ +WARNING [3003] Cannot evaluate % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule + Skipping % options because the User.Country attribute is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt b/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt new file mode 100644 index 0000000..8129521 --- /dev/null +++ b/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt @@ -0,0 +1,7 @@ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com","Country":"US"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule + Evaluating % options based on the User.Country attribute: + - Computing hash in the [0..99] range from User.Country => 63 (this value is sticky and consistent across all SDKs) + - Hash value 63 selects % option 1 (75%), 'Cat'. + Returning 'Cat'. diff --git a/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt b/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt new file mode 100644 index 0000000..74f812f --- /dev/null +++ b/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt @@ -0,0 +1,6 @@ +WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt b/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt new file mode 100644 index 0000000..ecb6047 --- /dev/null +++ b/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt @@ -0,0 +1,6 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Cat'. diff --git a/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt b/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt new file mode 100644 index 0000000..dd6032e --- /dev/null +++ b/test/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt @@ -0,0 +1,4 @@ +INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => no match + Returning 'Cat'. diff --git a/test/data/evaluation/prerequisite_flag.json b/test/data/evaluation/prerequisite_flag.json new file mode 100644 index 0000000..9c35c00 --- /dev/null +++ b/test/data/evaluation/prerequisite_flag.json @@ -0,0 +1,41 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", + "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", + "tests": [ + { + "key": "dependentFeatureWithUserCondition", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "prerequisite_flag_no_user_needed_by_dep.txt" + }, + { + "key": "dependentFeature", + "defaultValue": "default", + "returnValue": "Chicken", + "expectedLog": "prerequisite_flag_no_user_needed_by_prereq.txt" + }, + { + "key": "dependentFeatureWithUserCondition2", + "defaultValue": "default", + "returnValue": "Frog", + "expectedLog": "prerequisite_flag_no_user_needed_by_both.txt" + }, + { + "key": "dependentFeature", + "defaultValue": "default", + "user": { + "Identifier": "12345", + "Email": "kate@configcat.com", + "Country": "USA" + }, + "returnValue": "Horse", + "expectedLog": "prerequisite_flag.txt" + }, + { + "key": "dependentFeatureMultipleLevels", + "defaultValue": "default", + "returnValue": "Dog", + "expectedLog": "prerequisite_flag_multilevel.txt" + } + ] +} diff --git a/test/data/evaluation/prerequisite_flag/prerequisite_flag.txt b/test/data/evaluation/prerequisite_flag/prerequisite_flag.txt new file mode 100644 index 0000000..1d9022b --- /dev/null +++ b/test/data/evaluation/prerequisite_flag/prerequisite_flag.txt @@ -0,0 +1,32 @@ +INFO [5000] Evaluating 'dependentFeature' for User '{"Identifier":"12345","Email":"kate@configcat.com","Country":"USA"}' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeature' EQUALS 'target' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => no match + - IF User.Country IS ONE OF [<1 hashed value>] => true + AND User IS NOT IN SEGMENT 'Beta Users' + ( + Evaluating segment 'Beta Users': + - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions + Segment evaluation result: User IS NOT IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Beta Users') evaluates to true. + ) => true + AND User IS NOT IN SEGMENT 'Developers' + ( + Evaluating segment 'Developers': + - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions + Segment evaluation result: User IS NOT IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Developers') evaluates to true. + ) => true + THEN 'target' => MATCH, applying rule + Prerequisite flag evaluation result: 'target'. + Condition (Flag 'mainFeature' EQUALS 'target') evaluates to true. + ) + THEN % options => MATCH, applying rule + Evaluating % options based on the User.Identifier attribute: + - Computing hash in the [0..99] range from User.Identifier => 78 (this value is sticky and consistent across all SDKs) + - Hash value 78 selects % option 4 (25%), 'Horse'. + Returning 'Horse'. diff --git a/test/data/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt b/test/data/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt new file mode 100644 index 0000000..e9b9da6 --- /dev/null +++ b/test/data/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt @@ -0,0 +1,24 @@ +INFO [5000] Evaluating 'dependentFeatureMultipleLevels' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'intermediateFeature' EQUALS 'true' + ( + Evaluating prerequisite flag 'intermediateFeature': + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) => true + AND Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) => true + THEN 'true' => MATCH, applying rule + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'intermediateFeature' EQUALS 'true') evaluates to true. + ) + THEN 'Dog' => MATCH, applying rule + Returning 'Dog'. diff --git a/test/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt b/test/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt new file mode 100644 index 0000000..cbb04c7 --- /dev/null +++ b/test/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt @@ -0,0 +1,38 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition2' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'dependentFeatureWithUserCondition2' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeature' EQUALS 'public' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. + ) + THEN % options => MATCH, applying rule + Skipping % options because the User Object is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeature' EQUALS 'public' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. + ) + THEN 'Frog' => MATCH, applying rule + Returning 'Frog'. diff --git a/test/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt b/test/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt new file mode 100644 index 0000000..c86dc81 --- /dev/null +++ b/test/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt @@ -0,0 +1,15 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'dependentFeatureWithUserCondition' + Evaluating targeting rules and applying the first match if any: + - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'true' + ( + Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': + Prerequisite flag evaluation result: 'true'. + Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'true') evaluates to true. + ) + THEN % options => MATCH, applying rule + Skipping % options because the User Object is missing. + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Chicken'. diff --git a/test/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt b/test/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt new file mode 100644 index 0000000..1796e7c --- /dev/null +++ b/test/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt @@ -0,0 +1,18 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'dependentFeature' + Evaluating targeting rules and applying the first match if any: + - IF Flag 'mainFeature' EQUALS 'target' + ( + Evaluating prerequisite flag 'mainFeature': + Evaluating targeting rules and applying the first match if any: + - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'private' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions + THEN 'target' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Prerequisite flag evaluation result: 'public'. + Condition (Flag 'mainFeature' EQUALS 'target') evaluates to false. + ) + THEN % options => no match + Returning 'Chicken'. diff --git a/test/data/evaluation/segment.json b/test/data/evaluation/segment.json new file mode 100644 index 0000000..1bb4df5 --- /dev/null +++ b/test/data/evaluation/segment.json @@ -0,0 +1,47 @@ +{ + "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbd6ca-a85f-4ed0-888a-2da18def92b5/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", + "tests": [ + { + "key": "featureWithSegmentTargeting", + "defaultValue": false, + "returnValue": false, + "expectedLog": "segment_no_user.txt" + }, + { + "key": "featureWithSegmentTargetingMultipleConditions", + "defaultValue": false, + "returnValue": false, + "expectedLog": "segment_no_user_multi_conditions.txt" + }, + { + "key": "featureWithNegatedSegmentTargetingCleartext", + "defaultValue": false, + "user": { + "Identifier": "12345" + }, + "returnValue": false, + "expectedLog": "segment_no_targeted_attribute.txt" + }, + { + "key": "featureWithSegmentTargeting", + "defaultValue": false, + "user": { + "Identifier": "12345", + "Email": "jane@example.com" + }, + "returnValue": true, + "expectedLog": "segment_matching.txt" + }, + { + "key": "featureWithNegatedSegmentTargeting", + "defaultValue": false, + "user": { + "Identifier": "12345", + "Email": "jane@example.com" + }, + "returnValue": false, + "expectedLog": "segment_no_matching.txt" + } + ] +} diff --git a/test/data/evaluation/segment/segment_matching.txt b/test/data/evaluation/segment/segment_matching.txt new file mode 100644 index 0000000..9065aae --- /dev/null +++ b/test/data/evaluation/segment/segment_matching.txt @@ -0,0 +1,11 @@ +INFO [5000] Evaluating 'featureWithSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users' + ( + Evaluating segment 'Beta users': + - IF User.Email IS ONE OF [<2 hashed values>] => true + Segment evaluation result: User IS IN SEGMENT. + Condition (User IS IN SEGMENT 'Beta users') evaluates to true. + ) + THEN 'true' => MATCH, applying rule + Returning 'true'. diff --git a/test/data/evaluation/segment/segment_no_matching.txt b/test/data/evaluation/segment/segment_no_matching.txt new file mode 100644 index 0000000..0d04d83 --- /dev/null +++ b/test/data/evaluation/segment/segment_no_matching.txt @@ -0,0 +1,11 @@ +INFO [5000] Evaluating 'featureWithNegatedSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' + Evaluating targeting rules and applying the first match if any: + - IF User IS NOT IN SEGMENT 'Beta users' + ( + Evaluating segment 'Beta users': + - IF User.Email IS ONE OF [<2 hashed values>] => true + Segment evaluation result: User IS IN SEGMENT. + Condition (User IS NOT IN SEGMENT 'Beta users') evaluates to false. + ) + THEN 'true' => no match + Returning 'false'. diff --git a/test/data/evaluation/segment/segment_no_targeted_attribute.txt b/test/data/evaluation/segment/segment_no_targeted_attribute.txt new file mode 100644 index 0000000..6c7cf7e --- /dev/null +++ b/test/data/evaluation/segment/segment_no_targeted_attribute.txt @@ -0,0 +1,13 @@ +WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['jane@example.com', 'john@example.com']) for setting 'featureWithNegatedSegmentTargetingCleartext' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithNegatedSegmentTargetingCleartext' for User '{"Identifier":"12345"}' + Evaluating targeting rules and applying the first match if any: + - IF User IS NOT IN SEGMENT 'Beta users (cleartext)' + ( + Evaluating segment 'Beta users (cleartext)': + - IF User.Email IS ONE OF ['jane@example.com', 'john@example.com'] => false, skipping the remaining AND conditions + Segment evaluation result: cannot evaluate, the User.Email attribute is missing. + Condition (User IS NOT IN SEGMENT 'Beta users (cleartext)') failed to evaluate. + ) + THEN 'true' => cannot evaluate, the User.Email attribute is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/test/data/evaluation/segment/segment_no_user.txt b/test/data/evaluation/segment/segment_no_user.txt new file mode 100644 index 0000000..e8bd540 --- /dev/null +++ b/test/data/evaluation/segment/segment_no_user.txt @@ -0,0 +1,6 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargeting' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithSegmentTargeting' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users' THEN 'true' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/test/data/evaluation/segment/segment_no_user_multi_conditions.txt b/test/data/evaluation/segment/segment_no_user_multi_conditions.txt new file mode 100644 index 0000000..f09f9ce --- /dev/null +++ b/test/data/evaluation/segment/segment_no_user_multi_conditions.txt @@ -0,0 +1,7 @@ +WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargetingMultipleConditions' (User Object is missing). You should pass a User Object to the evaluation methods like `getValue()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ +INFO [5000] Evaluating 'featureWithSegmentTargetingMultipleConditions' + Evaluating targeting rules and applying the first match if any: + - IF User IS IN SEGMENT 'Beta users (cleartext)' => false, skipping the remaining AND conditions + THEN 'true' => cannot evaluate, User Object is missing + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'false'. diff --git a/test/data/evaluation/semver_validation.json b/test/data/evaluation/semver_validation.json new file mode 100644 index 0000000..3a14fc6 --- /dev/null +++ b/test/data/evaluation/semver_validation.json @@ -0,0 +1,26 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", + "tests": [ + { + "key": "isNotOneOf", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "semver_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "wrong_semver" + } + }, + { + "key": "relations", + "defaultValue": "default", + "returnValue": "Default", + "expectedLog": "semver_relations_error.txt", + "user": { + "Identifier": "12345", + "Custom1": "wrong_semver" + } + } + ] +} diff --git a/test/data/evaluation/semver_validation/semver_error.txt b/test/data/evaluation/semver_validation/semver_error.txt new file mode 100644 index 0000000..e14cc95 --- /dev/null +++ b/test/data/evaluation/semver_validation/semver_error.txt @@ -0,0 +1,9 @@ +WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', '']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +INFO [5000] Evaluating 'isNotOneOf' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', ''] THEN 'Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1'] THEN 'Is not one of (1.0.0, 3.0.1)' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. diff --git a/test/data/evaluation/semver_validation/semver_relations_error.txt b/test/data/evaluation/semver_validation/semver_relations_error.txt new file mode 100644 index 0000000..8198c85 --- /dev/null +++ b/test/data/evaluation/semver_validation/semver_relations_error.txt @@ -0,0 +1,18 @@ +WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0,') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 <= '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 > '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +WARNING [3004] Cannot evaluate condition (User.Custom1 >= '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. +INFO [5000] Evaluating 'relations' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' + Evaluating targeting rules and applying the first match if any: + - IF User.Custom1 < '1.0.0,' THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 < '1.0.0' THEN '< 1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 <= '1.0.0' THEN '<=1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 > '2.0.0' THEN '>2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + - IF User.Custom1 >= '2.0.0' THEN '>=2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) + The current targeting rule is ignored and the evaluation continues with the next rule. + Returning 'Default'. diff --git a/test/data/evaluation/simple_value.json b/test/data/evaluation/simple_value.json new file mode 100644 index 0000000..070d6f5 --- /dev/null +++ b/test/data/evaluation/simple_value.json @@ -0,0 +1,37 @@ +{ + "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", + "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", + "tests": [ + { + "key": "boolDefaultFalse", + "defaultValue": true, + "returnValue": false, + "expectedLog": "off_flag.txt" + }, + { + "key": "boolDefaultTrue", + "defaultValue": false, + "returnValue": true, + "expectedLog": "on_flag.txt" + }, + { + "key": "stringDefaultCat", + "defaultValue": "Default", + "returnValue": "Cat", + "expectedLog": "text_setting.txt" + }, + { + "key": "integerDefaultOne", + "defaultValue": 0, + "returnValue": 1, + "expectedLog": "int_setting.txt" + }, + { + "testName": "double_setting", + "key": "doubleDefaultPi", + "defaultValue": 0.0, + "returnValue": 3.1415, + "expectedLog": "double_setting.txt" + } + ] +} diff --git a/test/data/evaluation/simple_value/double_setting.txt b/test/data/evaluation/simple_value/double_setting.txt new file mode 100644 index 0000000..4a632f7 --- /dev/null +++ b/test/data/evaluation/simple_value/double_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'doubleDefaultPi' + Returning '3.1415'. diff --git a/test/data/evaluation/simple_value/int_setting.txt b/test/data/evaluation/simple_value/int_setting.txt new file mode 100644 index 0000000..1361843 --- /dev/null +++ b/test/data/evaluation/simple_value/int_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'integerDefaultOne' + Returning '1'. diff --git a/test/data/evaluation/simple_value/off_flag.txt b/test/data/evaluation/simple_value/off_flag.txt new file mode 100644 index 0000000..4580685 --- /dev/null +++ b/test/data/evaluation/simple_value/off_flag.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'boolDefaultFalse' + Returning 'false'. diff --git a/test/data/evaluation/simple_value/on_flag.txt b/test/data/evaluation/simple_value/on_flag.txt new file mode 100644 index 0000000..274c990 --- /dev/null +++ b/test/data/evaluation/simple_value/on_flag.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'boolDefaultTrue' + Returning 'true'. diff --git a/test/data/evaluation/simple_value/text_setting.txt b/test/data/evaluation/simple_value/text_setting.txt new file mode 100644 index 0000000..831d7c6 --- /dev/null +++ b/test/data/evaluation/simple_value/text_setting.txt @@ -0,0 +1,2 @@ +INFO [5000] Evaluating 'stringDefaultCat' + Returning 'Cat'. diff --git a/test/data/test.json b/test/data/test.json index d547507..99153a7 100644 --- a/test/data/test.json +++ b/test/data/test.json @@ -1,19 +1,34 @@ { "f": { "disabledFeature": { - "v": false + "t": 0, + "v": { + "b": false + } + }, + "doubleSetting": { + "t": 3, + "v": { + "d": 3.14 + } }, "enabledFeature": { - "v": true + "t": 0, + "v": { + "b": true + } }, "intSetting": { - "v": 5 - }, - "doubleSetting": { - "v": 3.14 + "t": 2, + "v": { + "i": 5 + } }, "stringSetting": { - "v": "test" + "t": 1, + "v": { + "s": "test" + } } } } \ No newline at end of file diff --git a/test/data/test_circulardependency_v6.json b/test/data/test_circulardependency_v6.json new file mode 100644 index 0000000..a8a9e17 --- /dev/null +++ b/test/data/test_circulardependency_v6.json @@ -0,0 +1,80 @@ +{ + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0 + }, + "f": { + "key1": { + "t": 1, + "v": { "s": "key1-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key1", + "c": 0, + "v": { "s": "key1-prereq" } + } + } + ], + "s": { "v": { "s": "key1-prereq" } } + } + ] + }, + "key2": { + "t": 1, + "v": { "s": "key2-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key3", + "c": 0, + "v": { "s": "key3-prereq" } + } + } + ], + "s": { "v": { "s": "key2-prereq" } } + } + ] + }, + "key3": { + "t": 1, + "v": { "s": "key3-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key2", + "c": 0, + "v": { "s": "key2-prereq" } + } + } + ], + "s": { "v": { "s": "key3-prereq" } } + } + ] + }, + "key4": { + "t": 1, + "v": { "s": "key4-value" }, + "r": [ + { + "c": [ + { + "p": { + "f": "key3", + "c": 0, + "v": { "s": "key3-prereq" } + } + } + ], + "s": { "v": { "s": "key4-prereq" } } + } + ] + } + } +} diff --git a/test/data/test_override_flagdependency_v6.json b/test/data/test_override_flagdependency_v6.json new file mode 100644 index 0000000..62e159e --- /dev/null +++ b/test/data/test_override_flagdependency_v6.json @@ -0,0 +1,44 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "TsTuRHo\u002BMHs8h8j16HQY83sooJsLg34Ir5KIVOletFU=" + }, + "f": { + "mainStringFlag": { + "t": 1, + "v": { + "s": "private" + }, + "i": "24c96275" + }, + "stringDependsOnInt": { + "t": 1, + "r": [ + { + "c": [ + { + "p": { + "f": "mainIntFlag", + "c": 0, + "v": { + "i": 42 + } + } + } + ], + "s": { + "v": { + "s": "Dog" + }, + "i": "12531eec" + } + } + ], + "v": { + "s": "Cat" + }, + "i": "e227d926" + } + } +} diff --git a/test/data/test_override_segments_v6.json b/test/data/test_override_segments_v6.json new file mode 100644 index 0000000..47bf15c --- /dev/null +++ b/test/data/test_override_segments_v6.json @@ -0,0 +1,66 @@ +{ + "p": { + "u": "https://test-cdn-eu.configcat.com", + "r": 0, + "s": "80xCU/SlDz1lCiWFaxIBjyJeJecWjq46T4eu6GtozkM=" + }, + "s": [ + { + "n": "Beta Users", + "r": [ + { + "a": "Email", + "c": 16, + "l": [ + "9189c42f6035bd1d2df5eda347a4f62926d27c80540a7aa6cc72cc75bc6757ff" + ] + } + ] + }, + { + "n": "Developers", + "r": [ + { + "a": "Email", + "c": 16, + "l": [ + "a7cdf54e74b5527bd2617889ec47f6d29b825ccfc97ff00832886bcb735abded" + ] + } + ] + } + ], + "f": { + "developerAndBetaUserSegment": { + "t": 0, + "r": [ + { + "c": [ + { + "s": { + "s": 1, + "c": 0 + } + }, + { + "s": { + "s": 0, + "c": 1 + } + } + ], + "s": { + "v": { + "b": true + }, + "i": "ddc50638" + } + } + ], + "v": { + "b": false + }, + "i": "6427f4b8" + } + } +} diff --git a/test/data/testmatrix.csv b/test/data/testmatrix.csv index 9f37c51..6de7454 100644 --- a/test/data/testmatrix.csv +++ b/test/data/testmatrix.csv @@ -1,1013 +1,1013 @@ Identifier;Email;Country;Custom1;bool30TrueAdvancedRules;boolDefaultFalse;boolDefaultTrue;double25Pi25E25Gr25Zero;doubleDefaultPi;integer25One25Two25Three25FourAdvancedRules;integerDefaultOne;string25Cat25Dog25Falcon25Horse;string25Cat25Dog25Falcon25HorseAdvancedRules;string75Cat0Dog25Falcon0Horse;stringContainsDogDefaultCat;stringDefaultCat;stringIsInDogDefaultCat;stringIsNotInDogDefaultCat;stringNotContainsDogDefaultCat -##null##;;;;true;false;true;-1;3.1415;-1;1;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat -;;;;false;false;true;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -a@configcat.com;a@configcat.com;Hungary;admin;false;false;true;5.561;3.1415;5;1;Cat;Dolphin;Cat;Dog;Cat;Dog;Cat;Cat -b@configcat.com;b@configcat.com;Hungary;;false;false;true;5.561;3.1415;5;1;Falcon;Dolphin;Cat;Dog;Cat;Dog;Cat;Cat -c@configcat.com;c@configcat.com;United Kingdom;admin;false;false;true;5.561;3.1415;5;1;Dog;Dolphin;Falcon;Dog;Cat;Dog;Dog;Cat -d@configcat.com;d@configcat.com;United Kingdom;;false;false;true;5.561;3.1415;5;1;Falcon;Dolphin;Cat;Dog;Cat;Cat;Dog;Cat -e@configcat.com;e@configcat.com;United States of America;;false;false;true;5.561;3.1415;5;1;Falcon;Kitten;Cat;Dog;Cat;Cat;Dog;Cat -f@configcat.com;f@configcat.com;;;false;false;true;5.561;3.1415;5;1;Cat;Kitten;Falcon;Dog;Cat;Cat;Dog;Cat -g@configcat.com;g@configcat.com;;;false;false;true;5.561;3.1415;5;1;Horse;Kitten;Cat;Dog;Cat;Cat;Dog;Cat -h@configcat.com;h@configcat.com;;;false;false;true;5.561;3.1415;5;1;Cat;Kitten;Cat;Dog;Cat;Cat;Dog;Cat -i@configcat.com;i@configcat.com;;admin;true;false;true;5.561;3.1415;5;1;Cat;Lion;Falcon;Dog;Cat;Dog;Dog;Cat -j@configcat.com;j@configcat.com;;;false;false;true;5.561;3.1415;5;1;Cat;Kitten;Falcon;Dog;Cat;Cat;Dog;Cat -stern@msn.com;stern@msn.com;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -sarahs@yahoo.com;sarahs@yahoo.com;##null##;##null##;true;false;true;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -luebke@hotmail.com;luebke@hotmail.com;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog -padme@icloud.com;padme@icloud.com;##null##;##null##;true;false;true;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -claypool@aol.com;claypool@aol.com;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -bogjobber@verizon.net;bogjobber@verizon.net;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -cliffordj@aol.com;cliffordj@aol.com;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog -bryanw@verizon.net;bryanw@verizon.net;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog -jfmulder@sbcglobal.net;jfmulder@sbcglobal.net;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -raines@live.com;raines@live.com;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog -ribet@yahoo.ca;ribet@yahoo.ca;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -nacho@gmail.com;nacho@gmail.com;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog -sherzodr@att.net;sherzodr@att.net;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -oster@optonline.net;oster@optonline.net;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -josem@icloud.com;josem@icloud.com;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog -hedwig@outlook.com;hedwig@outlook.com;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -camenisch@yahoo.com;camenisch@yahoo.com;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -ccohen@comcast.net;ccohen@comcast.net;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -techie@att.net;techie@att.net;##null##;##null##;false;false;true;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -damian@gmail.com;damian@gmail.com;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog -psharpe@comcast.net;psharpe@comcast.net;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -ebassi@me.com;ebassi@me.com;##null##;##null##;true;false;true;3.1415;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -curly@aol.com;curly@aol.com;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -rddesign@optonline.net;rddesign@optonline.net;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -boftx@gmail.com;boftx@gmail.com;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -eegsa@yahoo.ca;eegsa@yahoo.ca;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -ganter@gmail.com;ganter@gmail.com;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -mleary@att.net;mleary@att.net;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -kassiesa@icloud.com;kassiesa@icloud.com;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -peterhoeg@outlook.com;peterhoeg@outlook.com;##null##;##null##;false;false;true;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog -mhanoh@yahoo.ca;mhanoh@yahoo.ca;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -henkp@yahoo.com;henkp@yahoo.com;##null##;##null##;false;false;true;0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -krueger@sbcglobal.net;krueger@sbcglobal.net;##null##;##null##;false;false;true;0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Dog;Dog -barjam@yahoo.com;barjam@yahoo.com;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -mirod@msn.com;mirod@msn.com;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -marioph@yahoo.com;marioph@yahoo.com;##null##;##null##;false;false;true;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog -niknejad@optonline.net;niknejad@optonline.net;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog -bwcarty@sbcglobal.net;bwcarty@sbcglobal.net;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -mcast@aol.com;mcast@aol.com;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -portscan@msn.com;portscan@msn.com;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -pereinar@yahoo.ca;pereinar@yahoo.ca;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -floxy@verizon.net;floxy@verizon.net;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -mhassel@comcast.net;mhassel@comcast.net;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog -mgemmons@optonline.net;mgemmons@optonline.net;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -luvirini@mac.com;luvirini@mac.com;##null##;##null##;false;false;true;0;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -gslondon@gmail.com;gslondon@gmail.com;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -lamky@comcast.net;lamky@comcast.net;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -lipeng@aol.com;lipeng@aol.com;##null##;##null##;false;false;true;0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -keiji@mac.com;keiji@mac.com;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Dog;Dog -gumpish@verizon.net;gumpish@verizon.net;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog -tromey@hotmail.com;tromey@hotmail.com;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog -miyop@aol.com;miyop@aol.com;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -natepuri@me.com;natepuri@me.com;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -sbmrjbr@outlook.com;sbmrjbr@outlook.com;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Dog;Dog -hahiss@gmail.com;hahiss@gmail.com;##null##;##null##;false;false;true;0;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog -gmcgath@yahoo.ca;gmcgath@yahoo.ca;##null##;##null##;true;false;true;0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog -zavadsky@msn.com;zavadsky@msn.com;##null##;##null##;true;false;true;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -munson@gmail.com;munson@gmail.com;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -jfriedl@yahoo.com;jfriedl@yahoo.com;##null##;##null##;false;false;true;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -lushe@yahoo.ca;lushe@yahoo.ca;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -skythe@gmail.com;skythe@gmail.com;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -lipeng@aol.com;lipeng@aol.com;##null##;##null##;false;false;true;0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -jigsaw@me.com;jigsaw@me.com;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -schwaang@gmail.com;schwaang@gmail.com;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog -eurohack@verizon.net;eurohack@verizon.net;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog -janneh@icloud.com;janneh@icloud.com;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -frederic@me.com;frederic@me.com;##null##;##null##;false;false;true;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -facet@optonline.net;facet@optonline.net;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -uncle@aol.com;uncle@aol.com;##null##;##null##;false;false;true;0;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -wilsonpm@comcast.net;wilsonpm@comcast.net;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog -garland@optonline.net;garland@optonline.net;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -srour@yahoo.com;srour@yahoo.com;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog -inico@hotmail.com;inico@hotmail.com;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -plover@comcast.net;plover@comcast.net;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog -amichalo@comcast.net;amichalo@comcast.net;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog -mirod@hotmail.com;mirod@hotmail.com;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog -bester@mac.com;bester@mac.com;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -kildjean@verizon.net;kildjean@verizon.net;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Dog;Dog -arandal@comcast.net;arandal@comcast.net;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog -bartlett@yahoo.com;bartlett@yahoo.com;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -zyghom@icloud.com;zyghom@icloud.com;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -valdez@mac.com;valdez@mac.com;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -scato@yahoo.com;scato@yahoo.com;##null##;##null##;false;false;true;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog -sinkou@live.com;sinkou@live.com;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -evilopie@comcast.net;evilopie@comcast.net;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog -ducasse@gmail.com;ducasse@gmail.com;##null##;##null##;true;false;true;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog -sthomas@sbcglobal.net;sthomas@sbcglobal.net;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -plover@msn.com;plover@msn.com;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -mavilar@yahoo.com;mavilar@yahoo.com;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -josephw@msn.com;josephw@msn.com;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog -qmacro@yahoo.com;qmacro@yahoo.com;##null##;##null##;true;false;true;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -munson@mac.com;munson@mac.com;##null##;##null##;true;false;true;3.1415;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Dog;Dog -paulv@mac.com;paulv@mac.com;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -dogdude@hotmail.com;dogdude@hotmail.com;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Dog;Dog -symbolic@yahoo.ca;symbolic@yahoo.ca;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog -carcus@yahoo.com;carcus@yahoo.com;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -sblack@me.com;sblack@me.com;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Dog;Dog -richard@gmail.com;richard@gmail.com;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog -tbusch@yahoo.ca;tbusch@yahoo.ca;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog -gtaylor@aol.com;gtaylor@aol.com;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -burniske@att.net;burniske@att.net;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -bebing@me.com;bebing@me.com;##null##;##null##;false;false;true;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog -joglo@gmail.com;joglo@gmail.com;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -chrwin@sbcglobal.net;chrwin@sbcglobal.net;##null##;##null##;true;false;true;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -chaikin@yahoo.com;chaikin@yahoo.com;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -jigsaw@verizon.net;jigsaw@verizon.net;##null##;##null##;true;false;true;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog -wbarker@yahoo.ca;wbarker@yahoo.ca;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -ganter@verizon.net;ganter@verizon.net;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog -eegsa@att.net;eegsa@att.net;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog -sethbrown@hotmail.com;sethbrown@hotmail.com;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Dog;Dog -solomon@me.com;solomon@me.com;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog -tellis@yahoo.ca;tellis@yahoo.ca;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -jshirley@optonline.net;jshirley@optonline.net;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -tattooman@verizon.net;tattooman@verizon.net;##null##;##null##;false;false;true;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog -bescoto@yahoo.com;bescoto@yahoo.com;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog -hstiles@comcast.net;hstiles@comcast.net;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog -gumpish@optonline.net;gumpish@optonline.net;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog -hikoza@optonline.net;hikoza@optonline.net;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog -slanglois@yahoo.ca;slanglois@yahoo.ca;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog -kmiller@verizon.net;kmiller@verizon.net;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog -bowmanbs@hotmail.com;bowmanbs@hotmail.com;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog -penna@msn.com;penna@msn.com;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog -rnelson@live.com;rnelson@live.com;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog -johndo@aol.com;johndo@aol.com;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -shrapnull@att.net;shrapnull@att.net;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog -lcheng@comcast.net;lcheng@comcast.net;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -cyrus@msn.com;cyrus@msn.com;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog -suresh@yahoo.ca;suresh@yahoo.ca;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog -elflord@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -sassen@verizon.net;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dbindel@live.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -morain@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -timtroyr@outlook.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -esbeck@live.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -ilyaz@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -grinder@icloud.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -trieuvan@gmail.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -koudas@msn.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -specprog@yahoo.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -nichoj@outlook.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -sopwith@outlook.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -odlyzko@yahoo.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -warrior@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -budinger@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -lstein@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -kmiller@gmail.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -british@msn.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -webinc@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -kohlis@aol.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -matthijs@outlook.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mmccool@me.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -ribet@hotmail.com;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -wildfire@me.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -makarow@gmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -garland@hotmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -kjohnson@outlook.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -oneiros@sbcglobal.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -jaxweb@gmail.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -raides@msn.com;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -cantu@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -msherr@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dwsauder@aol.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -comdig@gmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -esokullu@yahoo.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kjetilk@aol.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -boomzilla@icloud.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -cvrcek@outlook.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -iamcal@yahoo.ca;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -grdschl@att.net;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -jgwang@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -stevelim@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -weidai@hotmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -dpitts@live.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -bebing@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -wikinerd@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -pfitza@yahoo.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -policies@me.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -niknejad@me.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -aukjan@hotmail.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dleconte@sbcglobal.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -noahb@aol.com;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bdbrown@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -adillon@att.net;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -eegsa@me.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -chunzi@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -privcan@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -mglee@hotmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -leocharre@me.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -dwendlan@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -lpalmer@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -emcleod@msn.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -breegster@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -mwandel@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -stewwy@me.com;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -drolsky@live.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -lukka@live.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -geekgrl@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -epeeist@me.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -papathan@verizon.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -singh@optonline.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -njpayne@aol.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -willg@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jimmichie@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -frosal@aol.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -dunstan@yahoo.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -parasite@yahoo.ca;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -firstpr@msn.com;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -konit@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -amaranth@msn.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -mcsporran@msn.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -gommix@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dprice@verizon.net;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -lcheng@me.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dwendlan@optonline.net;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -miami@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ajlitt@hotmail.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -grdschl@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -andersbr@att.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -nacho@yahoo.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hoangle@msn.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jbuchana@gmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -knorr@sbcglobal.net;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -saridder@gmail.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -scotfl@outlook.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -skoch@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -luebke@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -bsikdar@live.com;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -ryanvm@yahoo.ca;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dburrows@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -seebs@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -mgemmons@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -kobayasi@att.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -marcs@sbcglobal.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -netsfr@att.net;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -martink@me.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bflong@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -bhima@outlook.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -oster@att.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -teverett@yahoo.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kannan@optonline.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -yzheng@verizon.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -msusa@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -hmbrand@gmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -falcao@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -uraeus@live.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -dunstan@msn.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -oracle@yahoo.ca;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -tbeck@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -scottzed@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -sblack@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -yenya@msn.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bjoern@icloud.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -benanov@aol.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -preneel@outlook.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -punkis@sbcglobal.net;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -rwelty@comcast.net;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -penna@me.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -baveja@msn.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -louise@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -arachne@icloud.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hahiss@msn.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -wayward@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -pajas@sbcglobal.net;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -intlprog@comcast.net;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -hermanab@sbcglobal.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -presoff@msn.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -trygstad@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -denton@optonline.net;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -skythe@live.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -hmbrand@gmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -lushe@sbcglobal.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -magusnet@outlook.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ullman@optonline.net;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -jyoliver@optonline.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -flavell@icloud.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -ianbuck@att.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -satch@mac.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -gommix@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -rnelson@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -crusader@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -rddesign@sbcglobal.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -nanop@mac.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -ngedmond@live.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -munjal@live.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -unreal@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jemarch@sbcglobal.net;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -shawnce@sbcglobal.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -nweaver@yahoo.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -british@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ccohen@verizon.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -amaranth@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -louise@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -atmarks@optonline.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -iamcal@icloud.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -snunez@msn.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mrobshaw@optonline.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -denton@yahoo.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -konst@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -louise@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -wetter@gmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -kohlis@att.net;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -monkeydo@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -melnik@yahoo.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -munge@verizon.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -stefano@live.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -giafly@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -gbacon@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -dgriffith@yahoo.ca;##null##;##null##;##null##;true;false;true;1.61803;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dkrishna@icloud.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -firstpr@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -nichoj@comcast.net;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -aibrahim@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -inico@aol.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -ribet@sbcglobal.net;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ajlitt@mac.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -errxn@me.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -lstein@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -mgemmons@icloud.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jadavis@hotmail.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -zilla@hotmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -bogjobber@att.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -shazow@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -rgarton@sbcglobal.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -meder@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -hedwig@msn.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dhwon@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -mstrout@msn.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -manuals@me.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -andrewik@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hahsler@icloud.com;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -miami@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -facet@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -zeitlin@outlook.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -lamprecht@aol.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -johnh@mac.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mrsam@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -lipeng@outlook.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -dsowsy@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -philen@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kjohnson@gmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -nelson@hotmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -syncnine@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -pgottsch@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -jfmulder@yahoo.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jacks@yahoo.ca;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -mrdvt@outlook.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -cfhsoft@outlook.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -kodeman@yahoo.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -mbrown@comcast.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jaxweb@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -symbolic@icloud.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -denism@att.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -hager@mac.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -zavadsky@yahoo.com;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -mugwump@hotmail.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -geekgrl@comcast.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -dprice@me.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -petersko@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -netsfr@aol.com;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -tbmaddux@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -meder@att.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -benits@live.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -timlinux@gmail.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -heroine@att.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -hachi@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dkrishna@icloud.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -sthomas@gmail.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -thaljef@live.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -ganter@mac.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -drjlaw@live.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -timtroyr@gmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -smcnabb@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -fwiles@live.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -delpino@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -whimsy@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -lbecchi@gmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jbearp@yahoo.ca;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -tamas@live.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -panolex@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -lridener@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -wayward@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -amaranth@me.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -garland@yahoo.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -rfisher@live.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -stern@verizon.net;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -mavilar@yahoo.ca;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -rfisher@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -tarreau@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -koudas@sbcglobal.net;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -bwcarty@mac.com;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jeteve@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -pmint@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -barlow@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -punkis@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -jfinke@optonline.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -unreal@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -dwendlan@sbcglobal.net;##null##;##null##;##null##;true;false;true;3.1415;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dbrobins@yahoo.ca;##null##;##null##;##null##;true;false;true;1.61803;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -rasca@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jfriedl@att.net;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -clkao@outlook.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -noahb@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ducasse@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jrkorson@live.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hmbrand@gmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -corrada@yahoo.ca;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -tmaek@aol.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -richard@mac.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -pkplex@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kwilliams@icloud.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -mcrawfor@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -webteam@outlook.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -leakin@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ebassi@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -njpayne@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -konst@live.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -godeke@me.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -godeke@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -glenz@gmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -fallorn@comcast.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -nacho@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dkeeler@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -adhere@live.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -gfody@gmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -codex@verizon.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -erynf@msn.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -monkeydo@verizon.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -lauronen@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dinther@mac.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -miami@msn.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kewley@icloud.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dkeeler@outlook.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -galbra@gmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -mastinfo@yahoo.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kempsonc@sbcglobal.net;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -andale@mac.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -airship@sbcglobal.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -xtang@live.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jhardin@yahoo.ca;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -frederic@sbcglobal.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -matsn@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -pereinar@optonline.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -salesgeek@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -tezbo@aol.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -keijser@icloud.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -chaki@yahoo.ca;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -wetter@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -delpino@icloud.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -thassine@att.net;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -hoangle@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -bester@gmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -jdhedden@yahoo.ca;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -killmenow@msn.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -retoh@mac.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -goresky@yahoo.ca;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -microfab@att.net;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -pfitza@aol.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -onestab@hotmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -aracne@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -sherzodr@yahoo.ca;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -feamster@verizon.net;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -hyper@yahoo.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -jmgomez@me.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -fwitness@outlook.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -storerm@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -improv@yahoo.ca;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -arnold@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -blixem@live.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -rohitm@outlook.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -rfisher@verizon.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dwsauder@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -alastair@gmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -multiplx@hotmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -caidaperl@icloud.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -nacho@yahoo.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -keutzer@icloud.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -lbaxter@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -hachi@live.com;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -mfburgo@msn.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -gfody@yahoo.ca;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -jaxweb@yahoo.ca;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -wojciech@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -doche@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -josephw@hotmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -hakim@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -lstein@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -conteb@msn.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -wildixon@att.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -oechslin@hotmail.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -metzzo@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kosact@live.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kodeman@optonline.net;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -ebassi@hotmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -pgolle@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -jdhildeb@mac.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -garyjb@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -gslondon@me.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -maratb@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -marnanel@optonline.net;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -rgiersig@live.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -gozer@msn.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -valdez@hotmail.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -mnemonic@yahoo.ca;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -paina@outlook.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -syncnine@aol.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -melnik@gmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jaesenj@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -sekiya@me.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -tbusch@aol.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -whimsy@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -firstpr@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -bartlett@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -bester@att.net;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -gordonjcp@yahoo.ca;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -bcevc@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -sethbrown@me.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mcmillan@aol.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -raines@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -psharpe@comcast.net;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bachmann@gmail.com;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -leslie@att.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -hager@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ismail@mac.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -nacho@aol.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kohlis@yahoo.ca;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -lahvak@hotmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -gozer@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -willg@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -zavadsky@yahoo.ca;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -steve@me.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ccohen@icloud.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -msusa@mac.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -jsmith@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jshearer@outlook.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -pgottsch@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -majordick@gmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -sjava@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -rupak@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -empathy@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -intlprog@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -matthijs@gmail.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -portscan@sbcglobal.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -morain@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -gozer@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -akoblin@icloud.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mhassel@comcast.net;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -spadkins@sbcglobal.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -rohitm@yahoo.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -mwandel@yahoo.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -warrior@me.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jipsen@aol.com;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -bancboy@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -corrada@yahoo.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -wojciech@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -marcs@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -atmarks@me.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -quinn@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dkeeler@aol.com;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -pizza@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -empathy@mac.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -dmouse@aol.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -dinther@comcast.net;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -pappp@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -dougj@outlook.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -cfhsoft@msn.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -maratb@sbcglobal.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -stewwy@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -sravani@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -tmaek@msn.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -juliano@yahoo.ca;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mcsporran@optonline.net;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -mgemmons@yahoo.ca;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -seasweb@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -lushe@yahoo.ca;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -schumer@msn.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -gfody@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mlewan@yahoo.ca;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -eegsa@msn.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -rhavyn@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -cremonini@me.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -boftx@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -smartfart@outlook.com;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -uncled@outlook.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -quantaman@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -heidrich@live.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -moinefou@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ilial@mac.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -fraser@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -csilvers@me.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -csilvers@mac.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -kalpol@sbcglobal.net;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -punkis@yahoo.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -nacho@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -mcsporran@sbcglobal.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jaarnial@hotmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -gboss@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -henkp@msn.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -philb@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hllam@yahoo.com;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -roamer@yahoo.ca;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -geekgrl@comcast.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -cantu@outlook.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -firstpr@outlook.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -hmbrand@aol.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -arandal@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jaarnial@live.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -hoyer@me.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mmccool@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -smcnabb@att.net;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -pakaste@yahoo.ca;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -panolex@mac.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -hikoza@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -larry@verizon.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -arandal@outlook.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dwsauder@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -ducasse@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -policies@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -sblack@yahoo.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -hstiles@comcast.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -lushe@icloud.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -devphil@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dowdy@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -arachne@verizon.net;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -donev@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bowmanbs@hotmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -roesch@mac.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -lridener@aol.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -hmbrand@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -sopwith@hotmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -vsprintf@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dwsauder@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -symbolic@aol.com;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -gbacon@live.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -hillct@verizon.net;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -earmstro@att.net;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -studyabr@outlook.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -shawnce@yahoo.ca;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -boser@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -gknauss@yahoo.ca;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -marcs@att.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -bruck@icloud.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -comdig@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -floxy@yahoo.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -tmccarth@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -darin@me.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -mcraigw@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -fhirsch@outlook.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -unreal@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -crypt@comcast.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -pakaste@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -denism@att.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -staffelb@aol.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jonas@outlook.com;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -staikos@live.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mfburgo@me.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -psichel@sbcglobal.net;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -brainless@live.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -jmgomez@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -wsnyder@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -carcus@yahoo.ca;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dmouse@outlook.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -graham@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -murdocj@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -reziac@att.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -caronni@sbcglobal.net;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hoyer@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -amcuri@verizon.net;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -mstrout@live.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -osrin@verizon.net;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -geeber@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -konit@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -mxiao@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ryanshaw@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -bowmanbs@aol.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -yamla@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ardagna@sbcglobal.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -darin@outlook.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jmorris@me.com;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -valdez@att.net;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -haddawy@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -biglou@sbcglobal.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -pplinux@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -afeldspar@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -campbell@optonline.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -akoblin@outlook.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -nwiger@yahoo.ca;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -stinson@sbcglobal.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -daveed@me.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -arachne@outlook.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -augusto@live.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -xtang@me.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -itstatus@outlook.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ebassi@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kspiteri@yahoo.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -kewley@msn.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -yamla@me.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -oevans@me.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -ebassi@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -scitext@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -sisyphus@verizon.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -jandrese@live.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jamuir@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -dobey@yahoo.ca;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -rande@live.com;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dkasak@yahoo.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -greear@msn.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -thaljef@verizon.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -sjmuir@gmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -joehall@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -kronvold@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jmorris@outlook.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -ajohnson@yahoo.ca;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -russotto@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -pgolle@msn.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -mrdvt@aol.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -starstuff@icloud.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -jesse@live.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -luebke@outlook.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -emmanuel@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -imightb@msn.com;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -wbarker@sbcglobal.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -luvirini@hotmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -ylchang@yahoo.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -elflord@icloud.com;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -scottzed@yahoo.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mcraigw@yahoo.ca;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -nacho@icloud.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -rwelty@yahoo.ca;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -subir@aol.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -tbusch@yahoo.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -rupak@yahoo.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -giafly@aol.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -british@outlook.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -hllam@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -fatelk@att.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -parsimony@verizon.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -tbeck@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -citizenl@optonline.net;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -jimxugle@aol.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -starstuff@sbcglobal.net;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bader@icloud.com;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -starstuff@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -satishr@yahoo.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ilikered@gmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -bader@mac.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -yruan@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -skoch@outlook.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -bader@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -smallpaul@sbcglobal.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -stern@verizon.net;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -feamster@outlook.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -mcnihil@sbcglobal.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -firstpr@msn.com;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -bmorrow@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -laird@att.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -ingolfke@msn.com;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -irving@mac.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -monopole@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -keiji@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -wortmanj@gmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -keijser@me.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -aschmitz@yahoo.ca;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -tangsh@mac.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jdray@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -kewley@yahoo.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -policies@verizon.net;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -wayward@outlook.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -errxn@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -fglock@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -kmself@att.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -amichalo@gmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -mwandel@me.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -thowell@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -pedwards@yahoo.ca;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dmiller@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -choset@live.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dbrobins@aol.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -pizza@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -noahb@att.net;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jespley@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jfriedl@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -arebenti@sbcglobal.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -eidac@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jipsen@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -malin@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jramio@optonline.net;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jsnover@outlook.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -martyloo@gmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -danny@yahoo.ca;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bruck@hotmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jfmulder@live.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -wsnyder@msn.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -leslie@yahoo.ca;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -noticias@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -cgcra@yahoo.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -louise@me.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -rasca@verizon.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -cvrcek@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -chaffar@yahoo.ca;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -webdragon@att.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -keutzer@icloud.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -knorr@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -kawasaki@aol.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -ducasse@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -satishr@icloud.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -drewf@comcast.net;##null##;##null##;##null##;true;false;true;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -martyloo@yahoo.ca;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -lstein@live.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -nighthawk@me.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -debest@sbcglobal.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -cyrus@yahoo.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dogdude@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -yruan@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -bmidd@live.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -policies@att.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -treit@att.net;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -animats@msn.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kawasaki@sbcglobal.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jramio@yahoo.com;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -josephw@me.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -rgarcia@me.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -ryanvm@gmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -rnewman@me.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -yangyan@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -tubesteak@optonline.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -munjal@sbcglobal.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -pgolle@live.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -milton@icloud.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -harryh@live.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -howler@yahoo.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -drewf@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -cantu@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -leslie@optonline.net;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -mfleming@sbcglobal.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -nelson@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -valdez@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jsmith@msn.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -djpig@mac.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -bader@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -galbra@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dawnsong@msn.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -mcrawfor@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -pthomsen@icloud.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -raides@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ahuillet@icloud.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -kostas@aol.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -lridener@att.net;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -maneesh@outlook.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -sartak@yahoo.ca;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -rohitm@comcast.net;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -heidrich@mac.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -koudas@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -okroeger@me.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -cgcra@me.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -janusfury@yahoo.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -seurat@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dhrakar@mac.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -delpino@icloud.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -bebing@msn.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -keiji@gmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -okroeger@hotmail.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -gward@yahoo.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -jusdisgi@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -sakusha@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -rande@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mwitte@hotmail.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -alhajj@att.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -marcs@yahoo.ca;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -juerd@mac.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -sethbrown@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kdawson@verizon.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -mfburgo@aol.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -crandall@hotmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -schwaang@msn.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -mrsam@icloud.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -szymansk@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -epeeist@att.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -miyop@sbcglobal.net;##null##;##null##;##null##;true;false;true;3.1415;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -wainwrig@me.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -pereinar@icloud.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -killmenow@mac.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -dsowsy@mac.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -forsberg@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -amaranth@live.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -mkearl@hotmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jandrese@live.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -openldap@msn.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -world@hotmail.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -adamk@att.net;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -pdbaby@att.net;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -hellfire@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -firstpr@optonline.net;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kenja@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -leslie@gmail.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -bogjobber@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -hauma@verizon.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -hoangle@mac.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -nimaclea@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -fraterk@icloud.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -ninenine@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dogdude@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -campware@att.net;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -amimojo@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -karasik@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -yenya@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -stevelim@gmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -dvdotnet@att.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -bonmots@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -schumer@icloud.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -andrei@gmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -alfred@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -multiplx@optonline.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -tjensen@optonline.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -dmath@yahoo.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -kostas@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -carmena@gmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -terjesa@yahoo.ca;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -tjensen@sbcglobal.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -schwaang@aol.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -nimaclea@gmail.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jaesenj@live.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jimxugle@aol.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -mobileip@optonline.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -nullchar@aol.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -crowemojo@me.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -hermanab@msn.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -esbeck@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -naoya@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -lpalmer@att.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -dgatwood@aol.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -jtorkbob@att.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -rfoley@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -andale@comcast.net;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -mlewan@yahoo.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -ianbuck@yahoo.ca;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat -syrinx@live.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -imightb@live.com;##null##;##null##;##null##;true;false;true;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -gozer@icloud.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -gozer@outlook.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -shawnce@gmail.com;##null##;##null##;##null##;true;false;true;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -lauronen@att.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -raines@gmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jfriedl@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -hstiles@comcast.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -smpeters@icloud.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -thurston@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -flaviog@aol.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mnemonic@me.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -privcan@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -mobileip@verizon.net;##null##;##null##;##null##;true;false;true;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -gbacon@yahoo.ca;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -caronni@optonline.net;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -tbeck@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -keijser@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -scotfl@verizon.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -ryanshaw@sbcglobal.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -eimear@att.net;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -noticias@comcast.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -leocharre@yahoo.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat -killmenow@yahoo.ca;##null##;##null##;##null##;true;false;true;3.1415;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -petersen@gmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -bdthomas@yahoo.ca;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -mavilar@msn.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -isaacson@msn.com;##null##;##null##;##null##;false;false;true;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat -miyop@msn.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -bwcarty@hotmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -warrior@mac.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat -magusnet@hotmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -crowemojo@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -crypt@optonline.net;##null##;##null##;##null##;true;false;true;1.61803;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat -kempsonc@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat -kayvonf@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -noahb@yahoo.ca;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -symbolic@mac.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -wsnyder@yahoo.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -pdbaby@yahoo.ca;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -jfriedl@yahoo.com;##null##;##null##;##null##;false;false;true;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -openldap@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -jwarren@optonline.net;##null##;##null##;##null##;true;false;true;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -dsugal@verizon.net;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -kayvonf@aol.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat -nasarius@mac.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat -bolow@mac.com;##null##;##null##;##null##;true;false;true;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -tbmaddux@hotmail.com;##null##;##null##;##null##;true;false;true;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -maradine@aol.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -breegster@gmail.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat -breegster@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -openldap@gmail.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -jshirley@gmail.com;##null##;##null##;##null##;false;false;true;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat -tfinniga@msn.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat -delpino@mac.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat -stecoop@live.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat -jnolan@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -jhardin@yahoo.com;##null##;##null##;##null##;true;false;true;0;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -teverett@sbcglobal.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat -wsnyder@comcast.net;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -treeves@msn.com;##null##;##null##;##null##;false;false;true;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat -garland@outlook.com;##null##;##null##;##null##;false;false;true;0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat -ullman@comcast.net;##null##;##null##;##null##;false;false;true;3.1415;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat -sumdumass@outlook.com;##null##;##null##;##null##;false;false;true;2.7182;3.1415;1;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +##null##;;;;True;False;True;-1;3.1415;-1;1;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat +;;;;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +a@configcat.com;a@configcat.com;Hungary;admin;False;False;True;5.561;3.1415;5;1;Cat;Dolphin;Cat;Dog;Cat;Dog;Cat;Cat +b@configcat.com;b@configcat.com;Hungary;;False;False;True;5.561;3.1415;5;1;Falcon;Dolphin;Cat;Dog;Cat;Dog;Cat;Cat +c@configcat.com;c@configcat.com;United Kingdom;admin;False;False;True;5.561;3.1415;5;1;Dog;Dolphin;Falcon;Dog;Cat;Dog;Dog;Cat +d@configcat.com;d@configcat.com;United Kingdom;;False;False;True;5.561;3.1415;5;1;Falcon;Dolphin;Cat;Dog;Cat;Cat;Dog;Cat +e@configcat.com;e@configcat.com;United States of America;;False;False;True;5.561;3.1415;5;1;Falcon;Kitten;Cat;Dog;Cat;Cat;Dog;Cat +f@configcat.com;f@configcat.com;;;False;False;True;5.561;3.1415;5;1;Cat;Kitten;Falcon;Dog;Cat;Cat;Dog;Cat +g@configcat.com;g@configcat.com;;;False;False;True;5.561;3.1415;5;1;Horse;Kitten;Cat;Dog;Cat;Cat;Dog;Cat +h@configcat.com;h@configcat.com;;;False;False;True;5.561;3.1415;5;1;Cat;Kitten;Cat;Dog;Cat;Cat;Dog;Cat +i@configcat.com;i@configcat.com;;admin;True;False;True;5.561;3.1415;5;1;Cat;Lion;Falcon;Dog;Cat;Dog;Dog;Cat +j@configcat.com;j@configcat.com;;;False;False;True;5.561;3.1415;5;1;Cat;Kitten;Falcon;Dog;Cat;Cat;Dog;Cat +stern@msn.com;stern@msn.com;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +sarahs@yahoo.com;sarahs@yahoo.com;##null##;##null##;True;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +luebke@hotmail.com;luebke@hotmail.com;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +padme@icloud.com;padme@icloud.com;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +claypool@aol.com;claypool@aol.com;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +bogjobber@verizon.net;bogjobber@verizon.net;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +cliffordj@aol.com;cliffordj@aol.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog +bryanw@verizon.net;bryanw@verizon.net;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog +jfmulder@sbcglobal.net;jfmulder@sbcglobal.net;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +raines@live.com;raines@live.com;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog +ribet@yahoo.ca;ribet@yahoo.ca;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +nacho@gmail.com;nacho@gmail.com;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog +sherzodr@att.net;sherzodr@att.net;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +oster@optonline.net;oster@optonline.net;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +josem@icloud.com;josem@icloud.com;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog +hedwig@outlook.com;hedwig@outlook.com;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +camenisch@yahoo.com;camenisch@yahoo.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +ccohen@comcast.net;ccohen@comcast.net;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +techie@att.net;techie@att.net;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +damian@gmail.com;damian@gmail.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog +psharpe@comcast.net;psharpe@comcast.net;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +ebassi@me.com;ebassi@me.com;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +curly@aol.com;curly@aol.com;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +rddesign@optonline.net;rddesign@optonline.net;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +boftx@gmail.com;boftx@gmail.com;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +eegsa@yahoo.ca;eegsa@yahoo.ca;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +ganter@gmail.com;ganter@gmail.com;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Dog;Dog +mleary@att.net;mleary@att.net;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +kassiesa@icloud.com;kassiesa@icloud.com;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +peterhoeg@outlook.com;peterhoeg@outlook.com;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog +mhanoh@yahoo.ca;mhanoh@yahoo.ca;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +henkp@yahoo.com;henkp@yahoo.com;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +krueger@sbcglobal.net;krueger@sbcglobal.net;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Dog;Dog +barjam@yahoo.com;barjam@yahoo.com;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +mirod@msn.com;mirod@msn.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog +marioph@yahoo.com;marioph@yahoo.com;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog +niknejad@optonline.net;niknejad@optonline.net;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +bwcarty@sbcglobal.net;bwcarty@sbcglobal.net;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +mcast@aol.com;mcast@aol.com;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog +portscan@msn.com;portscan@msn.com;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +pereinar@yahoo.ca;pereinar@yahoo.ca;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +floxy@verizon.net;floxy@verizon.net;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +mhassel@comcast.net;mhassel@comcast.net;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog +mgemmons@optonline.net;mgemmons@optonline.net;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Dog;Dog +luvirini@mac.com;luvirini@mac.com;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +gslondon@gmail.com;gslondon@gmail.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +lamky@comcast.net;lamky@comcast.net;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Dog;Dog +lipeng@aol.com;lipeng@aol.com;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +keiji@mac.com;keiji@mac.com;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Dog;Dog +gumpish@verizon.net;gumpish@verizon.net;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog +tromey@hotmail.com;tromey@hotmail.com;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +miyop@aol.com;miyop@aol.com;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +natepuri@me.com;natepuri@me.com;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +sbmrjbr@outlook.com;sbmrjbr@outlook.com;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Dog;Dog +hahiss@gmail.com;hahiss@gmail.com;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog +gmcgath@yahoo.ca;gmcgath@yahoo.ca;##null##;##null##;True;False;True;0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog +zavadsky@msn.com;zavadsky@msn.com;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog +munson@gmail.com;munson@gmail.com;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +jfriedl@yahoo.com;jfriedl@yahoo.com;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +lushe@yahoo.ca;lushe@yahoo.ca;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Dog;Dog +skythe@gmail.com;skythe@gmail.com;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +lipeng@aol.com;lipeng@aol.com;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +jigsaw@me.com;jigsaw@me.com;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +schwaang@gmail.com;schwaang@gmail.com;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog +eurohack@verizon.net;eurohack@verizon.net;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog +janneh@icloud.com;janneh@icloud.com;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +frederic@me.com;frederic@me.com;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +facet@optonline.net;facet@optonline.net;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +uncle@aol.com;uncle@aol.com;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +wilsonpm@comcast.net;wilsonpm@comcast.net;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog +garland@optonline.net;garland@optonline.net;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +srour@yahoo.com;srour@yahoo.com;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog +inico@hotmail.com;inico@hotmail.com;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +plover@comcast.net;plover@comcast.net;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog +amichalo@comcast.net;amichalo@comcast.net;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +mirod@hotmail.com;mirod@hotmail.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog +bester@mac.com;bester@mac.com;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +kildjean@verizon.net;kildjean@verizon.net;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Dog;Dog +arandal@comcast.net;arandal@comcast.net;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +bartlett@yahoo.com;bartlett@yahoo.com;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +zyghom@icloud.com;zyghom@icloud.com;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +valdez@mac.com;valdez@mac.com;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog +scato@yahoo.com;scato@yahoo.com;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog +sinkou@live.com;sinkou@live.com;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog +evilopie@comcast.net;evilopie@comcast.net;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog +ducasse@gmail.com;ducasse@gmail.com;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog +sthomas@sbcglobal.net;sthomas@sbcglobal.net;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Dog;Dog +plover@msn.com;plover@msn.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +mavilar@yahoo.com;mavilar@yahoo.com;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog +josephw@msn.com;josephw@msn.com;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Dog;Dog +qmacro@yahoo.com;qmacro@yahoo.com;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +munson@mac.com;munson@mac.com;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Dog;Dog +paulv@mac.com;paulv@mac.com;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog +dogdude@hotmail.com;dogdude@hotmail.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Dog;Dog +symbolic@yahoo.ca;symbolic@yahoo.ca;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog +carcus@yahoo.com;carcus@yahoo.com;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +sblack@me.com;sblack@me.com;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Dog;Dog +richard@gmail.com;richard@gmail.com;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog +tbusch@yahoo.ca;tbusch@yahoo.ca;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog +gtaylor@aol.com;gtaylor@aol.com;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +burniske@att.net;burniske@att.net;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +bebing@me.com;bebing@me.com;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +joglo@gmail.com;joglo@gmail.com;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +chrwin@sbcglobal.net;chrwin@sbcglobal.net;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +chaikin@yahoo.com;chaikin@yahoo.com;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +jigsaw@verizon.net;jigsaw@verizon.net;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Dog;Dog +wbarker@yahoo.ca;wbarker@yahoo.ca;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +ganter@verizon.net;ganter@verizon.net;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog +eegsa@att.net;eegsa@att.net;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +sethbrown@hotmail.com;sethbrown@hotmail.com;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Dog;Dog +solomon@me.com;solomon@me.com;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Dog;Dog +tellis@yahoo.ca;tellis@yahoo.ca;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +jshirley@optonline.net;jshirley@optonline.net;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +tattooman@verizon.net;tattooman@verizon.net;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog +bescoto@yahoo.com;bescoto@yahoo.com;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Dog;Dog +hstiles@comcast.net;hstiles@comcast.net;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog +gumpish@optonline.net;gumpish@optonline.net;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Dog;Dog +hikoza@optonline.net;hikoza@optonline.net;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog +slanglois@yahoo.ca;slanglois@yahoo.ca;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Dog;Dog +kmiller@verizon.net;kmiller@verizon.net;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog +bowmanbs@hotmail.com;bowmanbs@hotmail.com;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Dog;Dog +penna@msn.com;penna@msn.com;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Dog;Dog +rnelson@live.com;rnelson@live.com;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Dog;Dog +johndo@aol.com;johndo@aol.com;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +shrapnull@att.net;shrapnull@att.net;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Dog;Dog +lcheng@comcast.net;lcheng@comcast.net;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +cyrus@msn.com;cyrus@msn.com;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Dog;Dog +suresh@yahoo.ca;suresh@yahoo.ca;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Dog;Dog +elflord@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +sassen@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +dbindel@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +morain@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +timtroyr@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +esbeck@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +ilyaz@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +grinder@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +trieuvan@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +koudas@msn.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +specprog@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +nichoj@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +sopwith@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +odlyzko@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +warrior@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +budinger@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +lstein@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +kmiller@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +british@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +webinc@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +kohlis@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +matthijs@outlook.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +mmccool@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +ribet@hotmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +wildfire@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +makarow@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +garland@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +kjohnson@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +oneiros@sbcglobal.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +jaxweb@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +raides@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +cantu@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +msherr@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +dwsauder@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +comdig@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +esokullu@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kjetilk@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +boomzilla@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +cvrcek@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +iamcal@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +grdschl@att.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jgwang@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +stevelim@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +weidai@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +dpitts@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +bebing@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +wikinerd@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +pfitza@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +policies@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +niknejad@me.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +aukjan@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dleconte@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +noahb@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bdbrown@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +adillon@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +eegsa@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +chunzi@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +privcan@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +mglee@hotmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +leocharre@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +dwendlan@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +lpalmer@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +emcleod@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +breegster@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mwandel@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +stewwy@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +drolsky@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +lukka@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +geekgrl@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +epeeist@me.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +papathan@verizon.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +singh@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +njpayne@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +willg@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jimmichie@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +frosal@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +dunstan@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +parasite@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +firstpr@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +konit@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +amaranth@msn.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mcsporran@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +gommix@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dprice@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +lcheng@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dwendlan@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +miami@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +ajlitt@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +grdschl@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +andersbr@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +nacho@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +hoangle@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jbuchana@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +knorr@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +saridder@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +scotfl@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +skoch@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +luebke@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +bsikdar@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ryanvm@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dburrows@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +seebs@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mgemmons@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +kobayasi@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +marcs@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +netsfr@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +martink@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bflong@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +bhima@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +oster@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +teverett@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kannan@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +yzheng@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +msusa@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +falcao@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +uraeus@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +dunstan@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +oracle@yahoo.ca;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +tbeck@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +scottzed@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +sblack@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +yenya@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bjoern@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +benanov@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +preneel@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +punkis@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +rwelty@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +penna@me.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +baveja@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +louise@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +arachne@icloud.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +hahiss@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +wayward@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +pajas@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +intlprog@comcast.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +hermanab@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +presoff@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +trygstad@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +denton@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +skythe@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +lushe@sbcglobal.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +magusnet@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ullman@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +jyoliver@optonline.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +flavell@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +ianbuck@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +satch@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +gommix@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +rnelson@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +crusader@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +rddesign@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +nanop@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +ngedmond@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +munjal@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +unreal@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jemarch@sbcglobal.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +shawnce@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +nweaver@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +british@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +ccohen@verizon.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +amaranth@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +louise@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +atmarks@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +iamcal@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +snunez@msn.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +mrobshaw@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +denton@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +konst@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +louise@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +wetter@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +kohlis@att.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +monkeydo@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +melnik@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +munge@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +stefano@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +giafly@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +gbacon@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +dgriffith@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dkrishna@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +firstpr@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +nichoj@comcast.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +aibrahim@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +inico@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +ribet@sbcglobal.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +ajlitt@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +errxn@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +lstein@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mgemmons@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jadavis@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +zilla@hotmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +bogjobber@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +shazow@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +rgarton@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +meder@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +hedwig@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +dhwon@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mstrout@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +manuals@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +andrewik@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +hahsler@icloud.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +miami@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +facet@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +zeitlin@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +lamprecht@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +johnh@mac.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +mrsam@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +lipeng@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +dsowsy@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +philen@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kjohnson@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +nelson@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +syncnine@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +pgottsch@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jfmulder@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jacks@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +mrdvt@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +cfhsoft@outlook.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +kodeman@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mbrown@comcast.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jaxweb@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +symbolic@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +denism@att.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +hager@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +zavadsky@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mugwump@hotmail.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +geekgrl@comcast.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +dprice@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +petersko@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +netsfr@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +tbmaddux@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +meder@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +benits@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +timlinux@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +heroine@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hachi@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dkrishna@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +sthomas@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +thaljef@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +ganter@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +drjlaw@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +timtroyr@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +smcnabb@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +fwiles@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +delpino@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +whimsy@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +lbecchi@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jbearp@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +tamas@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +panolex@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +lridener@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +wayward@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +amaranth@me.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +garland@yahoo.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +rfisher@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +stern@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +mavilar@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +rfisher@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +tarreau@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +koudas@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +bwcarty@mac.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jeteve@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +pmint@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +barlow@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +punkis@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jfinke@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +unreal@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +dwendlan@sbcglobal.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dbrobins@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +rasca@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jfriedl@att.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +clkao@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +noahb@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ducasse@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jrkorson@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +hmbrand@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +corrada@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +tmaek@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +richard@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +pkplex@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +kwilliams@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mcrawfor@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +webteam@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +leakin@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ebassi@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +njpayne@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +konst@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +godeke@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +godeke@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +glenz@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +fallorn@comcast.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +nacho@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dkeeler@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +adhere@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +gfody@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +codex@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +erynf@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +monkeydo@verizon.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +lauronen@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +dinther@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +miami@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +kewley@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +dkeeler@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +galbra@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mastinfo@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kempsonc@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +andale@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +airship@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +xtang@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jhardin@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +frederic@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +matsn@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +pereinar@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +salesgeek@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +tezbo@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +keijser@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +chaki@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +wetter@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +delpino@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +thassine@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +hoangle@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +bester@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jdhedden@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +killmenow@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +retoh@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +goresky@yahoo.ca;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +microfab@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +pfitza@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +onestab@hotmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +aracne@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +sherzodr@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +feamster@verizon.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +hyper@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jmgomez@me.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +fwitness@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +storerm@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +improv@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +arnold@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +blixem@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +rohitm@outlook.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +rfisher@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dwsauder@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +alastair@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +multiplx@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +caidaperl@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +nacho@yahoo.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +keutzer@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +lbaxter@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +hachi@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mfburgo@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +gfody@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +jaxweb@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +wojciech@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +doche@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +josephw@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +hakim@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +lstein@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +conteb@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +wildixon@att.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +oechslin@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +metzzo@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kosact@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +kodeman@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +ebassi@hotmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +pgolle@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jdhildeb@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +garyjb@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +gslondon@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +maratb@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +marnanel@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +rgiersig@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +gozer@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +valdez@hotmail.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +mnemonic@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +paina@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +syncnine@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +melnik@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jaesenj@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +sekiya@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +tbusch@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +whimsy@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +firstpr@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +bartlett@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +bester@att.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +gordonjcp@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +bcevc@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +sethbrown@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +mcmillan@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +raines@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +psharpe@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bachmann@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +leslie@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hager@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +ismail@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +nacho@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kohlis@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +lahvak@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +gozer@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +willg@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +zavadsky@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +steve@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ccohen@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +msusa@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jsmith@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +jshearer@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +pgottsch@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +majordick@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +sjava@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +rupak@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +empathy@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +intlprog@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +matthijs@gmail.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +portscan@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +morain@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +gozer@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +akoblin@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +mhassel@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +spadkins@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +rohitm@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mwandel@yahoo.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +warrior@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +jipsen@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +bancboy@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +corrada@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +wojciech@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +marcs@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +atmarks@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +quinn@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +dkeeler@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +pizza@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +empathy@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +dmouse@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +dinther@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +pappp@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +dougj@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +cfhsoft@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +maratb@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +stewwy@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +sravani@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +tmaek@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +juliano@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +mcsporran@optonline.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +mgemmons@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +seasweb@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +lushe@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +schumer@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +gfody@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +mlewan@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +eegsa@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +rhavyn@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +cremonini@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +boftx@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +smartfart@outlook.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +uncled@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +quantaman@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +heidrich@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +moinefou@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ilial@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +fraser@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +csilvers@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +csilvers@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +kalpol@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +punkis@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +nacho@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mcsporran@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jaarnial@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +gboss@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +henkp@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +philb@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +hllam@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +roamer@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +geekgrl@comcast.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +cantu@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +firstpr@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +hmbrand@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +arandal@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jaarnial@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +hoyer@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +mmccool@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +smcnabb@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +pakaste@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +panolex@mac.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hikoza@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +larry@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +arandal@outlook.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dwsauder@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ducasse@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +policies@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +sblack@yahoo.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +hstiles@comcast.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +lushe@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +devphil@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dowdy@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +arachne@verizon.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +donev@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bowmanbs@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +roesch@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +lridener@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hmbrand@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +sopwith@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +vsprintf@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dwsauder@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +symbolic@aol.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +gbacon@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hillct@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +earmstro@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +studyabr@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +shawnce@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +boser@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +gknauss@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +marcs@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +bruck@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +comdig@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +floxy@yahoo.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +tmccarth@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +darin@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +mcraigw@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +fhirsch@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +unreal@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +crypt@comcast.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +pakaste@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +denism@att.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +staffelb@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +jonas@outlook.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +staikos@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +mfburgo@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +psichel@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +brainless@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jmgomez@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +wsnyder@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +carcus@yahoo.ca;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dmouse@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +graham@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +murdocj@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +reziac@att.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +caronni@sbcglobal.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +hoyer@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +amcuri@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mstrout@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +osrin@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +geeber@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +konit@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +mxiao@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ryanshaw@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +bowmanbs@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +yamla@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ardagna@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +darin@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jmorris@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +valdez@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +haddawy@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +biglou@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +pplinux@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +afeldspar@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +campbell@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +akoblin@outlook.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +nwiger@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +stinson@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +daveed@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +arachne@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +augusto@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +xtang@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +itstatus@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ebassi@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +kspiteri@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +kewley@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +yamla@me.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +oevans@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +ebassi@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +scitext@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +sisyphus@verizon.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jandrese@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +jamuir@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +dobey@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +rande@live.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dkasak@yahoo.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +greear@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +thaljef@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +sjmuir@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +joehall@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +kronvold@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jmorris@outlook.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ajohnson@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +russotto@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +pgolle@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +mrdvt@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +starstuff@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +jesse@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +luebke@outlook.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +emmanuel@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +imightb@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +wbarker@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +luvirini@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +ylchang@yahoo.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +elflord@icloud.com;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +scottzed@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +mcraigw@yahoo.ca;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +nacho@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +rwelty@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +subir@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +tbusch@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +rupak@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +giafly@aol.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +british@outlook.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hllam@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +fatelk@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +parsimony@verizon.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +tbeck@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +citizenl@optonline.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jimxugle@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +starstuff@sbcglobal.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bader@icloud.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +starstuff@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +satishr@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +ilikered@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +bader@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +yruan@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +skoch@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +bader@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +smallpaul@sbcglobal.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +stern@verizon.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +feamster@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +mcnihil@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +firstpr@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +bmorrow@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +laird@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +ingolfke@msn.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +irving@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +monopole@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +keiji@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +wortmanj@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +keijser@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +aschmitz@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +tangsh@mac.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jdray@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +kewley@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +policies@verizon.net;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +wayward@outlook.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +errxn@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +fglock@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +kmself@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +amichalo@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mwandel@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +thowell@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +pedwards@yahoo.ca;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +dmiller@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +choset@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dbrobins@aol.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +pizza@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +noahb@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jespley@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +jfriedl@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +arebenti@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +eidac@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jipsen@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +malin@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +jramio@optonline.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jsnover@outlook.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +martyloo@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +danny@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bruck@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +jfmulder@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +wsnyder@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +leslie@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +noticias@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +cgcra@yahoo.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +louise@me.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +rasca@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +cvrcek@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +chaffar@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +webdragon@att.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +keutzer@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +knorr@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +kawasaki@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ducasse@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +satishr@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +drewf@comcast.net;##null##;##null##;##null##;True;False;True;0;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +martyloo@yahoo.ca;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +lstein@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +nighthawk@me.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +debest@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +cyrus@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +dogdude@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +yruan@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +bmidd@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +policies@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +treit@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +animats@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +kawasaki@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jramio@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +josephw@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +rgarcia@me.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +ryanvm@gmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +rnewman@me.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +yangyan@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +tubesteak@optonline.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +munjal@sbcglobal.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +pgolle@live.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +milton@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +harryh@live.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +howler@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +drewf@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +cantu@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +leslie@optonline.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mfleming@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +nelson@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +valdez@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jsmith@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +djpig@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +bader@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +galbra@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dawnsong@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mcrawfor@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +pthomsen@icloud.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +raides@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +ahuillet@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +kostas@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +lridener@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +maneesh@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +sartak@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +rohitm@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +heidrich@mac.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +koudas@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +okroeger@me.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +cgcra@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +janusfury@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +seurat@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dhrakar@mac.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +delpino@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +bebing@msn.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +keiji@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +okroeger@hotmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +gward@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +jusdisgi@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +sakusha@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +rande@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +mwitte@hotmail.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +alhajj@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +marcs@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +juerd@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +sethbrown@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +kdawson@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +mfburgo@aol.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +crandall@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +schwaang@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +mrsam@icloud.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +szymansk@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +epeeist@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +miyop@sbcglobal.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +wainwrig@me.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +pereinar@icloud.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +killmenow@mac.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +dsowsy@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +forsberg@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +amaranth@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +mkearl@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +jandrese@live.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +openldap@msn.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;3;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +world@hotmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +adamk@att.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +pdbaby@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hellfire@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +firstpr@optonline.net;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +kenja@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +leslie@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +bogjobber@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +hauma@verizon.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +hoangle@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +nimaclea@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +fraterk@icloud.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +ninenine@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dogdude@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +campware@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +amimojo@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +karasik@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +yenya@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +stevelim@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +dvdotnet@att.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +bonmots@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +schumer@icloud.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +andrei@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +alfred@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +multiplx@optonline.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +tjensen@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +dmath@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +kostas@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +carmena@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +terjesa@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +tjensen@sbcglobal.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +schwaang@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +nimaclea@gmail.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jaesenj@live.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jimxugle@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +mobileip@optonline.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +nullchar@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +crowemojo@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +hermanab@msn.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +esbeck@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +naoya@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +lpalmer@att.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +dgatwood@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +jtorkbob@att.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +rfoley@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Dog;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +andale@comcast.net;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +mlewan@yahoo.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +ianbuck@yahoo.ca;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Horse;Cat;Cat;Cat;Cat;Cat;Cat +syrinx@live.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +imightb@live.com;##null##;##null##;##null##;True;False;True;3.1415;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +gozer@icloud.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +gozer@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +shawnce@gmail.com;##null##;##null##;##null##;True;False;True;0;3.1415;4;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +lauronen@att.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +raines@gmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Falcon;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jfriedl@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +hstiles@comcast.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +smpeters@icloud.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +thurston@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +flaviog@aol.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +mnemonic@me.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +privcan@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +mobileip@verizon.net;##null##;##null##;##null##;True;False;True;0;3.1415;3;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +gbacon@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +caronni@optonline.net;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +tbeck@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +keijser@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +scotfl@verizon.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +ryanshaw@sbcglobal.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +eimear@att.net;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +noticias@comcast.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +leocharre@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Horse;Dog;Cat;Cat;Cat;Cat;Cat;Cat +killmenow@yahoo.ca;##null##;##null##;##null##;True;False;True;3.1415;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +petersen@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +bdthomas@yahoo.ca;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +mavilar@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +isaacson@msn.com;##null##;##null##;##null##;False;False;True;3.1415;3.1415;3;1;Horse;Horse;Cat;Cat;Cat;Cat;Cat;Cat +miyop@msn.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +bwcarty@hotmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +warrior@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;1;1;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +magusnet@hotmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;3;1;Cat;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +crowemojo@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +crypt@optonline.net;##null##;##null##;##null##;True;False;True;1.61803;3.1415;1;1;Cat;Dog;Falcon;Cat;Cat;Cat;Cat;Cat +kempsonc@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Falcon;Cat;Falcon;Cat;Cat;Cat;Cat;Cat +kayvonf@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +noahb@yahoo.ca;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +symbolic@mac.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +wsnyder@yahoo.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +pdbaby@yahoo.ca;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +jfriedl@yahoo.com;##null##;##null##;##null##;False;False;True;0;3.1415;4;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +openldap@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +jwarren@optonline.net;##null##;##null##;##null##;True;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +dsugal@verizon.net;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +kayvonf@aol.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Cat;Horse;Cat;Cat;Cat;Cat;Cat;Cat +nasarius@mac.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Dog;Cat;Cat;Cat;Cat;Cat;Cat;Cat +bolow@mac.com;##null##;##null##;##null##;True;False;True;1.61803;3.1415;2;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +tbmaddux@hotmail.com;##null##;##null##;##null##;True;False;True;2.7182;3.1415;2;1;Horse;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +maradine@aol.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +breegster@gmail.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +breegster@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;4;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +openldap@gmail.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;4;1;Falcon;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +jshirley@gmail.com;##null##;##null##;##null##;False;False;True;0;3.1415;2;1;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Cat +tfinniga@msn.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;4;1;Dog;Horse;Cat;Cat;Cat;Cat;Cat;Cat +delpino@mac.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;3;1;Falcon;Cat;Cat;Cat;Cat;Cat;Cat;Cat +stecoop@live.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;2;1;Falcon;Dog;Cat;Cat;Cat;Cat;Cat;Cat +jnolan@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;3;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +jhardin@yahoo.com;##null##;##null##;##null##;True;False;True;0;3.1415;1;1;Dog;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +teverett@sbcglobal.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;2;1;Horse;Horse;Falcon;Cat;Cat;Cat;Cat;Cat +wsnyder@comcast.net;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +treeves@msn.com;##null##;##null##;##null##;False;False;True;1.61803;3.1415;2;1;Cat;Falcon;Cat;Cat;Cat;Cat;Cat;Cat +garland@outlook.com;##null##;##null##;##null##;False;False;True;0;3.1415;1;1;Horse;Cat;Cat;Cat;Cat;Cat;Cat;Cat +ullman@comcast.net;##null##;##null##;##null##;False;False;True;3.1415;3.1415;1;1;Horse;Falcon;Falcon;Cat;Cat;Cat;Cat;Cat +sumdumass@outlook.com;##null##;##null##;##null##;False;False;True;2.7182;3.1415;1;1;Dog;Horse;Falcon;Cat;Cat;Cat;Cat;Cat diff --git a/test/data/testmatrix_and_or.csv b/test/data/testmatrix_and_or.csv new file mode 100644 index 0000000..5a149f4 --- /dev/null +++ b/test/data/testmatrix_and_or.csv @@ -0,0 +1,15 @@ +Identifier;Email;Country;Custom1;mainFeature;dependentFeature;emailAnd;emailOr +##null##;;;;public;Chicken;Cat;Cat +;;;;public;Chicken;Cat;Cat +jane@example.com;jane@example.com;##null##;##null##;public;Chicken;Cat;Jane +john@example.com;john@example.com;##null##;##null##;public;Chicken;Cat;John +a@example.com;a@example.com;USA;##null##;target;Cat;Cat;Cat +mark@example.com;mark@example.com;USA;##null##;target;Dog;Cat;Mark +nora@example.com;nora@example.com;USA;##null##;target;Falcon;Cat;Cat +stern@msn.com;stern@msn.com;USA;##null##;target;Horse;Cat;Cat +jane@sensitivecompany.com;jane@sensitivecompany.com;England;##null##;private;Chicken;Dog;Jane +anna@sensitivecompany.com;anna@sensitivecompany.com;France;##null##;private;Chicken;Cat;Cat +jane@sensitivecompany.com;jane@sensitivecompany.com;england;##null##;public;Chicken;Dog;Jane +jane;jane;##null##;##null##;public;Chicken;Cat;Cat +@sensitivecompany.com;@sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat +jane.sensitivecompany.com;jane.sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat diff --git a/test/data/testmatrix_comparators_v6.csv b/test/data/testmatrix_comparators_v6.csv new file mode 100644 index 0000000..d53efb5 --- /dev/null +++ b/test/data/testmatrix_comparators_v6.csv @@ -0,0 +1,24 @@ +Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringEqualsCleartextDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringNotEqualsCleartextDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute;stringContainsAnyOfDogDefaultCat;stringNotContainsAnyOfDogDefaultCat;stringStartsWithAnyOfDogDefaultCat;stringStartsWithAnyOfCleartextDogDefaultCat;stringNotStartsWithAnyOfDogDefaultCat;stringNotStartsWithAnyOfCleartextDogDefaultCat;stringEndsWithAnyOfDogDefaultCat;stringEndsWithAnyOfCleartextDogDefaultCat;stringNotEndsWithAnyOfDogDefaultCat;stringNotEndsWithAnyOfCleartextDogDefaultCat;stringArrayContainsAnyOfDogDefaultCat;stringArrayContainsAnyOfCleartextDogDefaultCat;stringArrayNotContainsAnyOfDogDefaultCat;stringArrayNotContainsAnyOfCleartextDogDefaultCat +##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat +a@configcat.com;a@configcat.com;##null##;##null##;False;Dog;Dog;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat +reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +writer@configcat.com;writer@configcat.com;China;["Write"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog +admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +admin@configcat.com;admin@configcat.com;France;["Read", "Write", "execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +admin@configcat.com;admin@configcat.com;France;["Read", "Write", "eXecute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog +user@configcat.com;user@configcat.com;Greece;["","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat +user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat diff --git a/test/data/testmatrix_prerequisite_flag.csv b/test/data/testmatrix_prerequisite_flag.csv new file mode 100644 index 0000000..dcf68f4 --- /dev/null +++ b/test/data/testmatrix_prerequisite_flag.csv @@ -0,0 +1,5 @@ +Identifier;Email;Country;Custom1;mainBoolFlag;mainStringFlag;mainIntFlag;mainDoubleFlag;stringDependsOnBool;stringDependsOnString;stringDependsOnStringCaseCheck;stringDependsOnInt;stringDependsOnDouble;stringDependsOnDoubleIntValue;boolDependsOnBool;intDependsOnBool;doubleDependsOnBool;boolDependsOnBoolDependsOnBool;mainBoolFlagEmpty;stringDependsOnEmptyBool;stringInverseDependsOnEmptyBool;mainBoolFlagInverse;boolDependsOnBoolInverse +##null##;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True +;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True +john@sensitivecompany.com;john@sensitivecompany.com;##null##;##null##;False;private;2;0.1;Cat;Dog;Cat;Dog;Dog;Cat;False;42;3.14;True;True;EmptyOn;EmptyOn;True;False +jane@example.com;jane@example.com;##null##;##null##;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True diff --git a/test/data/testmatrix_segments.csv b/test/data/testmatrix_segments.csv new file mode 100644 index 0000000..b59ba3a --- /dev/null +++ b/test/data/testmatrix_segments.csv @@ -0,0 +1,6 @@ +Identifier;Email;Country;Custom1;developerAndBetaUserSegment;developerAndBetaUserCleartextSegment;notDeveloperAndNotBetaUserSegment;notDeveloperAndNotBetaUserCleartextSegment +##null##;;;;False;False;False;False +;;;;False;False;False;False +john@example.com;john@example.com;##null##;##null##;False;False;False;False +jane@example.com;jane@example.com;##null##;##null##;False;False;False;False +kate@example.com;kate@example.com;##null##;##null##;True;True;True;True diff --git a/test/data/testmatrix_segments_old.csv b/test/data/testmatrix_segments_old.csv new file mode 100644 index 0000000..9fc605e --- /dev/null +++ b/test/data/testmatrix_segments_old.csv @@ -0,0 +1,6 @@ +Identifier;Email;Country;Custom1;featureWithSegmentTargeting;featureWithSegmentTargetingCleartext;featureWithNegatedSegmentTargeting;featureWithNegatedSegmentTargetingCleartext;featureWithSegmentTargetingInverse;featureWithSegmentTargetingInverseCleartext;featureWithNegatedSegmentTargetingInverse;featureWithNegatedSegmentTargetingInverseCleartext +##null##;;;;False;False;False;False;False;False;False;False +;;;;False;False;False;False;False;False;False;False +john@example.com;john@example.com;##null##;##null##;True;True;False;False;False;False;True;True +jane@example.com;jane@example.com;##null##;##null##;True;True;False;False;False;False;True;True +kate@example.com;kate@example.com;##null##;##null##;False;False;True;True;True;True;False;False diff --git a/test/data/testmatrix_unicode.csv b/test/data/testmatrix_unicode.csv new file mode 100644 index 0000000..e5b01de --- /dev/null +++ b/test/data/testmatrix_unicode.csv @@ -0,0 +1,14 @@ +Identifier;Email;Country;🆃🅴🆇🆃;boolTextEqualsHashed;boolTextEqualsCleartext;boolTextNotEqualsHashed;boolTextNotEqualsCleartext;boolIsOneOfHashed;boolIsOneOfCleartext;boolIsNotOneOfHashed;boolIsNotOneOfCleartext;boolStartsWithHashed;boolStartsWithCleartext;boolNotStartsWithHashed;boolNotStartsWithCleartext;boolEndsWithHashed;boolEndsWithCleartext;boolNotEndsWithHashed;boolNotEndsWithCleartext;boolContainsCleartext;boolNotContainsCleartext;boolArrayContainsHashed;boolArrayContainsCleartext;boolArrayNotContainsHashed;boolArrayNotContainsCleartext +1;;;ʄǟռƈʏ ȶɛӼȶ;True;True;False;False;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False +1;;;ʄaռƈʏ ȶɛӼȶ;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False +1;;;ÁRVÍZTŰRŐ tükörfúrógép;False;False;True;True;True;True;False;False;True;True;False;False;True;True;False;False;True;False;False;False;False;False +1;;;árvíztűrő tükörfúrógép;False;False;True;True;False;False;True;True;False;False;True;True;True;True;False;False;True;False;False;False;False;False +1;;;ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP;False;False;True;True;False;False;True;True;True;True;False;False;False;False;True;True;True;False;False;False;False;False +1;;;árvíztűrő TÜKÖRFÚRÓGÉP;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False +1;;;u𝖓𝖎𝖈𝖔𝖉e;False;False;True;True;True;True;False;False;True;True;False;False;True;True;False;False;True;False;False;False;False;False +;;;𝖚𝖓𝖎𝖈𝖔𝖉e;False;False;True;True;False;False;True;True;False;False;True;True;True;True;False;False;True;False;False;False;False;False +;;;u𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;True;True;False;False;False;False;True;True;True;False;False;False;False;False +;;;𝖚𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False +1;;;["ÁRVÍZTŰRŐ tükörfúrógép", "unicode"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;True;True;False;False +1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "u𝖓𝖎𝖈𝖔𝖉e"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;True;True;False;False +1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "unicode"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;False;False;True;True diff --git a/test/mock.h b/test/mock.h index abcebc9..2ba2c64 100644 --- a/test/mock.h +++ b/test/mock.h @@ -10,6 +10,7 @@ #include "configcat/httpsessionadapter.h" #include "configcat/config.h" #include "configcat/evaluationdetails.h" +#include "configcatlogger.h" class InMemoryConfigCache : public configcat::ConfigCache { public: @@ -45,11 +46,12 @@ class HookCallbacks { public: bool isReady = false; int isReadyCallCount = 0; - std::shared_ptr changedConfig; + std::shared_ptr changedConfig; int changedConfigCallCount = 0; - configcat::EvaluationDetails evaluationDetails; + configcat::EvaluationDetails<> evaluationDetails; int evaluationDetailsCallCount = 0; - std::string error; + std::string errorMessage; + std::exception_ptr errorException; int errorCallCount = 0; void onClientReady() { @@ -57,18 +59,19 @@ class HookCallbacks { isReadyCallCount += 1; } - void onConfigChanged(std::shared_ptr config) { + void onConfigChanged(std::shared_ptr config) { changedConfig = config; changedConfigCallCount += 1; } - void onFlagEvaluated(const configcat::EvaluationDetails& details) { - evaluationDetails = details; + void onFlagEvaluated(const configcat::EvaluationDetailsBase& details) { + evaluationDetails = to_concrete(details); evaluationDetailsCallCount += 1; } - void onError(const std::string& error) { - this->error = error; + void onError(const std::string& error, const std::exception_ptr& exception) { + errorMessage = error; + errorException = exception; errorCallCount += 1; } }; @@ -129,37 +132,107 @@ class MockHttpSessionAdapter : public configcat::HttpSessionAdapter { std::atomic closed = false; }; +class TestLogger : public configcat::ILogger { + public: + TestLogger(configcat::LogLevel level = configcat::LOG_LEVEL_INFO): ILogger(level) {} + void log(configcat::LogLevel level, const std::string& message, const std::exception_ptr& exception = nullptr) override { + text += logLevelAsString(level) + std::string(" ") + message; + if (exception) + text += std::string("Exception details: ") + unwrap_exception_message(exception); + text += "\n"; + } + + void clear() { + text.clear(); + } + + std::string text; +}; + static constexpr char kTestJsonString[] = R"({ - "p": { - "u": "https://cdn-global.configcat.com", - "r": 0 + "p": { + "u": "https://cdn-global.configcat.com", + "r": 0 + }, + "f": { + "key1": { + "t": 0, + "v": { + "b": true + }, + "i": "fakeId1" }, - "f": { - "testBoolKey": { - "v": true, "t": 0, "p": [], "r": [] - }, - "testStringKey": { - "v": "testValue", "i": "id", "t": 1, "p": [], - "r": [ - { - "i": "id1", "v": "fake1", "a": "Identifier", "t": 2, "c": "@test1.com" - }, - { - "i": "id2", "v": "fake2", "a": "Identifier", "t": 2, "c": "@test2.com" - } - ] - }, - "testIntKey": { - "v": 1, "t": 2, "p": [], "r": [] - }, - "testDoubleKey": { - "v": 1.1, "t": 3, "p": [], "r": [] - }, - "key1": { - "v": true, "i": "fakeId1", "p": [], "r": [] + "key2": { + "t": 0, + "v": { + "b": false + }, + "i": "fakeId2" + }, + "testBoolKey": { + "t": 0, + "v": { + "b": true + } + }, + "testDoubleKey": { + "t": 3, + "v": { + "d": 1.1 + } + }, + "testIntKey": { + "t": 2, + "v": { + "i": 1 + } + }, + "testStringKey": { + "t": 1, + "r": [ + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "@test1.com" + ] + } + } + ], + "s": { + "v": { + "s": "fake1" + }, + "i": "id1" + } }, - "key2": { - "v": false, "i": "fakeId2", "p": [], "r": [] + { + "c": [ + { + "u": { + "a": "Identifier", + "c": 2, + "l": [ + "@test2.com" + ] + } + } + ], + "s": { + "v": { + "s": "fake2" + }, + "i": "id2" + } } + ], + "v": { + "s": "testValue" + }, + "i": "id" } + } })"; diff --git a/test/test-autopolling.cpp b/test/test-autopolling.cpp index e70ead6..2d3a019 100644 --- a/test/test-autopolling.cpp +++ b/test/test-autopolling.cpp @@ -3,7 +3,7 @@ #include "utils.h" #include "configservice.h" #include "configcat/configcatoptions.h" -#include "configcat/configcatlogger.h" +#include "configcatlogger.h" #include "configcat/consolelogger.h" #include #include @@ -16,16 +16,16 @@ using namespace std::this_thread; class AutoPollingTest : public ::testing::Test { public: static constexpr char kTestSdkKey[] = "TestSdkKey"; - static constexpr char kTestJsonFormat[] = R"({ "f": { "fakeKey": { "v": %s, "p": [], "r": [] } } })"; + static constexpr char kTestJsonFormat[] = R"({"f":{"fakeKey":{"t":%d,"v":%s}}})"; shared_ptr mockHttpSessionAdapter = make_shared(); shared_ptr logger = make_shared(make_shared(), make_shared()); }; TEST_F(AutoPollingTest, Get) { - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(secondResponse); ConfigCatOptions options; @@ -43,9 +43,9 @@ TEST_F(AutoPollingTest, Get) { } TEST_F(AutoPollingTest, GetFailedRequest) { - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {500, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {500, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(secondResponse); ConfigCatOptions options; @@ -63,9 +63,9 @@ TEST_F(AutoPollingTest, GetFailedRequest) { } TEST_F(AutoPollingTest, OnConfigChanged) { - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(secondResponse); bool called = false; @@ -87,7 +87,7 @@ TEST_F(AutoPollingTest, OnConfigChanged) { } TEST_F(AutoPollingTest, RequestTimeout) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; constexpr int responseDelay = 3; mockHttpSessionAdapter->enqueueResponse(response, responseDelay); @@ -108,7 +108,7 @@ TEST_F(AutoPollingTest, RequestTimeout) { } TEST_F(AutoPollingTest, InitWaitTimeout) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; constexpr int responseDelay = 5; mockHttpSessionAdapter->enqueueResponse(response, responseDelay); @@ -129,7 +129,7 @@ TEST_F(AutoPollingTest, InitWaitTimeout) { } TEST_F(AutoPollingTest, CancelRequest) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; constexpr int responseDelay = 60; mockHttpSessionAdapter->enqueueResponse(response, responseDelay); @@ -148,9 +148,9 @@ TEST_F(AutoPollingTest, CancelRequest) { TEST_F(AutoPollingTest, Cache) { auto mockCache = make_shared(); - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(secondResponse); ConfigCatOptions options; @@ -162,7 +162,7 @@ TEST_F(AutoPollingTest, Cache) { EXPECT_EQ("test", std::get(settings["fakeKey"].value)); EXPECT_EQ(1, mockCache->store.size()); - EXPECT_TRUE(contains(mockCache->store.begin()->second, R"("test")")); + EXPECT_TRUE(contains(mockCache->store.begin()->second, R"({"s":"test"})")); sleep_for(seconds(3)); @@ -170,19 +170,19 @@ TEST_F(AutoPollingTest, Cache) { EXPECT_EQ("test2", std::get(settings["fakeKey"].value)); EXPECT_EQ(1, mockCache->store.size()); - EXPECT_TRUE(contains(mockCache->store.begin()->second, R"("test2")")); + EXPECT_TRUE(contains(mockCache->store.begin()->second, R"({"s":"test2"})")); } TEST_F(AutoPollingTest, ReturnCachedConfigWhenCacheIsNotExpired) { - auto jsonString = string_format(kTestJsonFormat, R"("test")"); + auto jsonString = string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})"); auto mockCache = make_shared(ConfigEntry( Config::fromJson(jsonString), "test-etag", jsonString, - getUtcNowSecondsSinceEpoch()).serialize() + get_utcnowseconds_since_epoch()).serialize() ); - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); auto pollIntervalSeconds = 2; @@ -216,15 +216,15 @@ TEST_F(AutoPollingTest, ReturnCachedConfigWhenCacheIsNotExpired) { TEST_F(AutoPollingTest, FetchConfigWhenCacheIsExpired) { auto pollIntervalSeconds = 2; auto maxInitWaitTimeSeconds = 1; - auto jsonString = string_format(kTestJsonFormat, R"("test")"); + auto jsonString = string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})"); auto mockCache = make_shared(ConfigEntry( Config::fromJson(jsonString), "test-etag", jsonString, - getUtcNowSecondsSinceEpoch() - pollIntervalSeconds).serialize() + get_utcnowseconds_since_epoch() - pollIntervalSeconds).serialize() ); - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); ConfigCatOptions options; @@ -240,15 +240,15 @@ TEST_F(AutoPollingTest, FetchConfigWhenCacheIsExpired) { TEST_F(AutoPollingTest, initWaitTimeReturnCached) { auto pollIntervalSeconds = 60; auto maxInitWaitTimeSeconds = 1; - auto jsonString = string_format(kTestJsonFormat, R"("test")"); + auto jsonString = string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})"); auto mockCache = make_shared(ConfigEntry( Config::fromJson(jsonString), "test-etag", jsonString, - getUtcNowSecondsSinceEpoch() - 2 * pollIntervalSeconds).serialize() + get_utcnowseconds_since_epoch() - 2 * pollIntervalSeconds).serialize() ); - configcat::Response response = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; constexpr int responseDelay = 5; mockHttpSessionAdapter->enqueueResponse(response, responseDelay); @@ -271,7 +271,7 @@ TEST_F(AutoPollingTest, initWaitTimeReturnCached) { } TEST_F(AutoPollingTest, OnlineOffline) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(response); ConfigCatOptions options; @@ -301,7 +301,7 @@ TEST_F(AutoPollingTest, OnlineOffline) { } TEST_F(AutoPollingTest, InitOffline) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(response); ConfigCatOptions options; diff --git a/test/test-configcache.cpp b/test/test-configcache.cpp index 84bbcc6..1d007c3 100644 --- a/test/test-configcache.cpp +++ b/test/test-configcache.cpp @@ -1,17 +1,17 @@ #include #include "mock.h" -#include "utils.h" #include "configservice.h" #include "configcat/configcatoptions.h" #include "configcat/configcatclient.h" +#include "configcat/timeutils.h" using namespace configcat; using namespace std; TEST(ConfigCacheTest, CacheKey) { - EXPECT_EQ("147c5b4c2b2d7c77e1605b1a4309f0ea6684a0c6", ConfigService::generateCacheKey("test1")); - EXPECT_EQ("c09513b1756de9e4bc48815ec7a142b2441ed4d5", ConfigService::generateCacheKey("test2")); + EXPECT_EQ("f83ba5d45bceb4bb704410f51b704fb6dfa19942", ConfigService::generateCacheKey("configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012")); + EXPECT_EQ("da7bfd8662209c8ed3f9db96daed4f8d91ba5876", ConfigService::generateCacheKey("configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012")); } TEST(ConfigCacheTest, CachePayload) { @@ -22,41 +22,44 @@ TEST(ConfigCacheTest, CachePayload) { } TEST(ConfigCatTest, InvalidCacheContent) { - static constexpr char kTestJsonFormat[] = R"({ "f": { "testKey": { "v": %s, "p": [], "r": [] } } })"; + static constexpr char kTestJsonFormat[] = R"({"f":{"testKey":{"t":%d,"v":%s}}})"; HookCallbacks hookCallbacks; auto hooks = make_shared(); - hooks->addOnError([&](const string& error) { hookCallbacks.onError(error); }); - auto configJsonString = string_format(kTestJsonFormat, R"("test")"); + hooks->addOnError([&](const string& message, const exception_ptr& exception) { hookCallbacks.onError(message, exception); }); + auto configJsonString = string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})"); auto configCache = make_shared(ConfigEntry( Config::fromJson(configJsonString), "test-etag", configJsonString, - getUtcNowSecondsSinceEpoch()).serialize() + get_utcnowseconds_since_epoch()).serialize() ); ConfigCatOptions options; options.pollingMode = PollingMode::manualPoll(); options.configCache = configCache; options.hooks = hooks; - auto client = ConfigCatClient::get("test", &options); + auto client = ConfigCatClient::get("test-67890123456789012/1234567890123456789012", &options); EXPECT_EQ("test", client->getValue("testKey", "default")); EXPECT_EQ(0, hookCallbacks.errorCallCount); // Invalid fetch time in cache - configCache->value = "text\n"s + "test-etag\n" + string_format(kTestJsonFormat, R"("test2")"); + configCache->value = "text\n"s + "test-etag\n" + string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})"); EXPECT_EQ("test", client->getValue("testKey", "default")); - EXPECT_TRUE(hookCallbacks.error.find("Error occurred while reading the cache. Invalid fetch time: text") != std::string::npos); + EXPECT_EQ("Error occurred while reading the cache.", hookCallbacks.errorMessage); + EXPECT_TRUE(unwrap_exception_message(hookCallbacks.errorException).find("Invalid fetch time: text") != string::npos); // Number of values is fewer than expected - configCache->value = std::to_string(getUtcNowSecondsSinceEpoch()) + "\n" + string_format(kTestJsonFormat, R"("test2")"); + configCache->value = std::to_string(get_utcnowseconds_since_epoch()) + "\n" + string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})"); EXPECT_EQ("test", client->getValue("testKey", "default")); - EXPECT_TRUE(hookCallbacks.error.find("Error occurred while reading the cache. Number of values is fewer than expected.") != std::string::npos); + EXPECT_TRUE(hookCallbacks.errorMessage.find("Error occurred while reading the cache.") != std::string::npos); + EXPECT_TRUE(unwrap_exception_message(hookCallbacks.errorException).find("Number of values is fewer than expected.") != string::npos); // Invalid config JSON - configCache->value = std::to_string(getUtcNowSecondsSinceEpoch()) + "\n" + "test-etag\n" + "wrong-json"; + configCache->value = std::to_string(get_utcnowseconds_since_epoch()) + "\n" + "test-etag\n" + "wrong-json"; EXPECT_EQ("test", client->getValue("testKey", "default")); - EXPECT_TRUE(hookCallbacks.error.find("Error occurred while reading the cache. Invalid config JSON: wrong-json.") != std::string::npos); + EXPECT_TRUE(hookCallbacks.errorMessage.find("Error occurred while reading the cache.") != std::string::npos); + EXPECT_TRUE(unwrap_exception_message(hookCallbacks.errorException).find("Invalid config JSON: wrong-json.") != string::npos); ConfigCatClient::close(client); } diff --git a/test/test-configcatclient.cpp b/test/test-configcatclient.cpp index 9b6e7c3..563136a 100644 --- a/test/test-configcatclient.cpp +++ b/test/test-configcatclient.cpp @@ -2,7 +2,6 @@ #include "mock.h" #include "configcat/configcat.h" #include "configfetcher.h" -#include "utils.h" #include using namespace configcat; @@ -11,11 +10,11 @@ using namespace std::this_thread; class ConfigCatClientTest : public ::testing::Test { public: - static constexpr char kTestSdkKey[] = "TestSdkKey"; - static constexpr char kTestJsonFormat[] = R"({ "f": { "fakeKey": { "v": %s, "p": [], "r": [] } } })"; - static constexpr char kTestJsonMultiple[] = R"({ "f": { "key1": { "v": true, "i": "fakeId1", "p": [], "r": [] }, "key2": { "v": false, "i": "fakeId2", "p": [], "r": [] } } })"; + static constexpr char kTestSdkKey[] = "TestSdkKey-23456789012/1234567890123456789012"; + static constexpr char kTestJsonFormat[] = R"({"f":{"fakeKey":{"t":%d,"v":%s}}})"; + static constexpr char kTestJsonMultiple[] = R"({"f":{"key1":{"t":0,"v":{"b":true},"i":"fakeId1"},"key2":{"t":0,"v":{"b":false},"i":"fakeId2"}}})"; - ConfigCatClient* client = nullptr; + shared_ptr client = nullptr; shared_ptr mockHttpSessionAdapter = make_shared(); void SetUp(const std::string& sdkKey = kTestSdkKey) { @@ -39,28 +38,66 @@ TEST_F(ConfigCatClientTest, EnsureSingletonPerSdkKey) { } TEST_F(ConfigCatClientTest, EnsureCloseWorks) { - auto client = ConfigCatClient::get("another"); - auto client2 = ConfigCatClient::get("another"); + auto client = ConfigCatClient::get("another-90123456789012/1234567890123456789012"); + auto client2 = ConfigCatClient::get("another-90123456789012/1234567890123456789012"); EXPECT_TRUE(client2 == client); - EXPECT_TRUE(ConfigCatClient::instanceCount() == 1); + EXPECT_EQ(1, ConfigCatClient::instanceCount()); ConfigCatClient::close(client2); - EXPECT_TRUE(ConfigCatClient::instanceCount() == 0); + EXPECT_EQ(0, ConfigCatClient::instanceCount()); - client = ConfigCatClient::get("another"); - EXPECT_TRUE(ConfigCatClient::instanceCount() == 1); + client = ConfigCatClient::get("another-90123456789012/1234567890123456789012"); + EXPECT_EQ(1, ConfigCatClient::instanceCount()); ConfigCatClient::closeAll(); - EXPECT_TRUE(ConfigCatClient::instanceCount() == 0); + EXPECT_EQ(0, ConfigCatClient::instanceCount()); + + client = ConfigCatClient::get("another-90123456789012/1234567890123456789012"); + EXPECT_EQ(1, ConfigCatClient::instanceCount()); +} + +class SdkKeyFormatValidationTestSuite : public ::testing::TestWithParam> {}; +INSTANTIATE_TEST_SUITE_P(ConfigCatClientTest, SdkKeyFormatValidationTestSuite, ::testing::Values( + make_tuple("sdk-key-90123456789012", false, false), + make_tuple("sdk-key-9012345678901/1234567890123456789012", false, false), + make_tuple("sdk-key-90123456789012/123456789012345678901", false, false), + make_tuple("sdk-key-90123456789012/12345678901234567890123", false, false), + make_tuple("sdk-key-901234567890123/1234567890123456789012", false, false), + make_tuple("sdk-key-90123456789012/1234567890123456789012", false, true), + make_tuple("configcat-sdk-1/sdk-key-90123456789012", false, false), + make_tuple("configcat-sdk-1/sdk-key-9012345678901/1234567890123456789012", false, false), + make_tuple("configcat-sdk-1/sdk-key-90123456789012/123456789012345678901", false, false), + make_tuple("configcat-sdk-1/sdk-key-90123456789012/12345678901234567890123", false, false), + make_tuple("configcat-sdk-1/sdk-key-901234567890123/1234567890123456789012", false, false), + make_tuple("configcat-sdk-1/sdk-key-90123456789012/1234567890123456789012", false, true), + make_tuple("configcat-sdk-2/sdk-key-90123456789012/1234567890123456789012", false, false), + make_tuple("configcat-proxy/", false, false), + make_tuple("configcat-proxy/", true, false), + make_tuple("configcat-proxy/sdk-key-90123456789012", false, false), + make_tuple("configcat-proxy/sdk-key-90123456789012", true, true) +)); +TEST_P(SdkKeyFormatValidationTestSuite, SdkKeyFormatValidation) { + auto [sdkKey, customBaseUrl, isValid] = GetParam(); + try { + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + options.baseUrl = customBaseUrl ? "https://my-configcat-proxy" : ""; + auto client = ConfigCatClient::get(sdkKey, &options); - client = ConfigCatClient::get("another"); - EXPECT_TRUE(ConfigCatClient::instanceCount() == 1); + if (!isValid) { + FAIL() << "Expected invalid_argument exception"; + } + } catch (const invalid_argument&) { + if (isValid) { + FAIL() << "Did not expect invalid_argument exception"; + } + } } TEST_F(ConfigCatClientTest, GetIntValue) { SetUp(); - configcat::Response response = {200, string_format(kTestJsonFormat, "43")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::Int, R"({"i":43})")}; mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); auto value = client->getValue("fakeKey", 10); @@ -71,7 +108,7 @@ TEST_F(ConfigCatClientTest, GetIntValue) { TEST_F(ConfigCatClientTest, GetIntValueFailed) { SetUp(); - configcat::Response response = {200, string_format(kTestJsonFormat, R"("fake")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake"})")}; mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); auto value = client->getValue("fakeKey", 10); @@ -93,7 +130,7 @@ TEST_F(ConfigCatClientTest, GetIntValueFailedInvalidJson) { TEST_F(ConfigCatClientTest, GetIntValueFailedPartialJson) { SetUp(); - configcat::Response responseWithoutValue = {200, R"({ "f": { "fakeKey": { "p": [], "r": [] } } })"}; + configcat::Response responseWithoutValue = {200, R"({"f":{"fakeKey":{"p":[],"r":[]}}}})"}; mockHttpSessionAdapter->enqueueResponse(responseWithoutValue); client->forceRefresh(); auto value = client->getValue("fakeKey", 10); @@ -104,7 +141,7 @@ TEST_F(ConfigCatClientTest, GetIntValueFailedPartialJson) { TEST_F(ConfigCatClientTest, GetIntValueFailedNullValueJson) { SetUp(); - configcat::Response responseWithoutValue = {200, R"({ "f": { "fakeKey": { "v": null, "p": [], "r": [] } } })"}; + configcat::Response responseWithoutValue = {200, R"({"f":{"fakeKey":{"p":[],"r":[],"v":null}}}})"}; mockHttpSessionAdapter->enqueueResponse(responseWithoutValue); client->forceRefresh(); auto value = client->getValue("fakeKey", 10); @@ -115,7 +152,7 @@ TEST_F(ConfigCatClientTest, GetIntValueFailedNullValueJson) { TEST_F(ConfigCatClientTest, GetStringValue) { SetUp(); - configcat::Response response = {200, string_format(kTestJsonFormat, R"("fake")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake"})")}; mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); auto value = client->getValue("fakeKey", "default"); @@ -126,7 +163,7 @@ TEST_F(ConfigCatClientTest, GetStringValue) { TEST_F(ConfigCatClientTest, GetStringValueFailed) { SetUp(); - configcat::Response response = {200, string_format(kTestJsonFormat, "33")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::Int, R"({"i":33})")}; mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); auto value = client->getValue("fakeKey", "default"); @@ -137,7 +174,7 @@ TEST_F(ConfigCatClientTest, GetStringValueFailed) { TEST_F(ConfigCatClientTest, GetDoubleValue) { SetUp(); - configcat::Response response = {200, string_format(kTestJsonFormat, "43.56")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::Double, R"({"d":43.56})")}; mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); auto value = client->getValue("fakeKey", 3.14); @@ -148,7 +185,7 @@ TEST_F(ConfigCatClientTest, GetDoubleValue) { TEST_F(ConfigCatClientTest, GetDoubleValueFailed) { SetUp(); - configcat::Response response = {200, string_format(kTestJsonFormat, R"("fake")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake"})")}; mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); auto value = client->getValue("fakeKey", 3.14); @@ -159,7 +196,7 @@ TEST_F(ConfigCatClientTest, GetDoubleValueFailed) { TEST_F(ConfigCatClientTest, GetBoolValue) { SetUp(); - configcat::Response response = {200, string_format(kTestJsonFormat, "true")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::Boolean, R"({"b":true})")}; mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); auto value = client->getValue("fakeKey", false); @@ -170,7 +207,7 @@ TEST_F(ConfigCatClientTest, GetBoolValue) { TEST_F(ConfigCatClientTest, GetBoolValueFailed) { SetUp(); - configcat::Response response = {200, string_format(kTestJsonFormat, R"("fake")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake"})")}; mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); auto value = client->getValue("fakeKey", false); @@ -181,25 +218,31 @@ TEST_F(ConfigCatClientTest, GetBoolValueFailed) { TEST_F(ConfigCatClientTest, GetLatestOnFail) { SetUp(); - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"(55)")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::Int, R"({"i":55})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); configcat::Response secondResponse = {500, ""}; mockHttpSessionAdapter->enqueueResponse(secondResponse); - client->forceRefresh(); + auto refreshResult = client->forceRefresh(); + EXPECT_TRUE(refreshResult.success()); + EXPECT_FALSE(refreshResult.errorMessage.has_value()); + EXPECT_EQ(nullptr, refreshResult.errorException); auto value = client->getValue("fakeKey", 0); EXPECT_EQ(55, value); - client->forceRefresh(); + refreshResult = client->forceRefresh(); + EXPECT_FALSE(refreshResult.success()); + EXPECT_TRUE(refreshResult.errorMessage.has_value()); + EXPECT_EQ(nullptr, refreshResult.errorException); value = client->getValue("fakeKey", 0); EXPECT_EQ(55, value); } TEST_F(ConfigCatClientTest, ForceRefreshLazy) { - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(secondResponse); ConfigCatOptions options; @@ -217,9 +260,9 @@ TEST_F(ConfigCatClientTest, ForceRefreshLazy) { } TEST_F(ConfigCatClientTest, ForceRefreshAuto) { - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(secondResponse); ConfigCatOptions options; @@ -251,9 +294,9 @@ TEST_F(ConfigCatClientTest, FailingAutoPoll) { TEST_F(ConfigCatClientTest, FromCacheOnly) { auto mockCache = make_shared(); auto cacheKey = SHA1()(""s + kTestSdkKey + "_" + ConfigFetcher::kConfigJsonName + "_" + ConfigEntry::kSerializationFormatVersion); - auto jsonString = string_format(kTestJsonFormat, R"("fake")"); + auto jsonString = string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake"})"); auto config = Config::fromJson(jsonString); - auto configEntry = ConfigEntry(config, "test-etag", jsonString, getUtcNowSecondsSinceEpoch()); + auto configEntry = ConfigEntry(config, "test-etag", jsonString, get_utcnowseconds_since_epoch()); mockCache->write(cacheKey, configEntry.serialize()); mockHttpSessionAdapter->enqueueResponse({500, ""}); @@ -270,9 +313,9 @@ TEST_F(ConfigCatClientTest, FromCacheOnly) { TEST_F(ConfigCatClientTest, FromCacheOnlyRefresh) { auto mockCache = make_shared(); auto cacheKey = SHA1()(""s + kTestSdkKey + "_" + ConfigFetcher::kConfigJsonName + "_" + ConfigEntry::kSerializationFormatVersion); - auto jsonString = string_format(kTestJsonFormat, R"("fake")"); + auto jsonString = string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake"})"); auto config = Config::fromJson(jsonString); - auto configEntry = ConfigEntry(config, "test-etag", jsonString, getUtcNowSecondsSinceEpoch()); + auto configEntry = ConfigEntry(config, "test-etag", jsonString, get_utcnowseconds_since_epoch()); mockCache->write(cacheKey, configEntry.serialize()); mockHttpSessionAdapter->enqueueResponse({500, ""}); @@ -347,7 +390,7 @@ TEST_F(ConfigCatClientTest, GetAllValueDetails) { client->forceRefresh(); auto allDetails = client->getAllValueDetails(); - auto details_by_key = [&](const std::vector& all_details, const std::string& key) -> const EvaluationDetails* { + auto details_by_key = [&](const std::vector>& all_details, const std::string& key) -> const EvaluationDetails* { for (const auto& details : all_details) { if (details.key == key) { return &details; @@ -371,7 +414,7 @@ TEST_F(ConfigCatClientTest, GetAllValueDetails) { details = details_by_key(allDetails, "testIntKey"); EXPECT_NE(nullptr, details); EXPECT_EQ("testIntKey", details->key); - EXPECT_EQ(1, get(details->value)); + EXPECT_EQ(1, get(details->value)); details = details_by_key(allDetails, "testDoubleKey"); EXPECT_NE(nullptr, details); @@ -398,27 +441,30 @@ TEST_F(ConfigCatClientTest, GetValueDetails) { mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); - ConfigCatUser user("test@test1.com"); - auto details = client->getValueDetails("testStringKey", "", &user); + auto user = make_shared("test@test1.com"); + auto details = client->getValueDetails("testStringKey", "", user); - EXPECT_EQ("fake1", get(details.value)); + EXPECT_EQ("fake1", details.value); EXPECT_EQ("testStringKey", details.key); EXPECT_EQ("id1", details.variationId); EXPECT_FALSE(details.isDefaultValue); - EXPECT_TRUE(details.error.empty()); - EXPECT_TRUE(details.matchedEvaluationPercentageRule == std::nullopt); - EXPECT_EQ("fake1", get(details.matchedEvaluationRule->value)); - EXPECT_EQ(CONTAINS, details.matchedEvaluationRule->comparator); - EXPECT_EQ("Identifier", details.matchedEvaluationRule->comparisonAttribute); - EXPECT_EQ("@test1.com", details.matchedEvaluationRule->comparisonValue); - EXPECT_EQ(user.toJson(), details.user->toJson()); + EXPECT_FALSE(details.errorMessage.has_value()); + EXPECT_TRUE(details.matchedPercentageOption == std::nullopt); + + auto& condition = get(details.matchedTargetingRule->conditions[0].condition); + auto& simpleValue = get(details.matchedTargetingRule->then); + EXPECT_EQ("fake1", get(simpleValue.value)); + EXPECT_EQ(UserComparator::TextContainsAnyOf, condition.comparator); + EXPECT_EQ("Identifier", condition.comparisonAttribute); + EXPECT_EQ("@test1.com", get>(condition.comparisonValue)[0]); + EXPECT_EQ(user->toJson(), details.user->toJson()); std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); EXPECT_GE(now, details.fetchTime); EXPECT_LE(now, details.fetchTime + std::chrono::seconds(1)); } TEST_F(ConfigCatClientTest, AutoPollUserAgentHeader) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("fake")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake"})")}; mockHttpSessionAdapter->enqueueResponse(response); ConfigCatOptions options; @@ -433,7 +479,7 @@ TEST_F(ConfigCatClientTest, AutoPollUserAgentHeader) { } TEST_F(ConfigCatClientTest, LazyPollUserAgentHeader) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("fake")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake"})")}; mockHttpSessionAdapter->enqueueResponse(response); ConfigCatOptions options; @@ -448,7 +494,7 @@ TEST_F(ConfigCatClientTest, LazyPollUserAgentHeader) { } TEST_F(ConfigCatClientTest, ManualPollUserAgentHeader) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("fake")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake"})")}; mockHttpSessionAdapter->enqueueResponse(response); ConfigCatOptions options; @@ -463,7 +509,7 @@ TEST_F(ConfigCatClientTest, ManualPollUserAgentHeader) { } TEST_F(ConfigCatClientTest, Concurrency_DoNotStartNewFetchIfThereIsAnOngoingFetch) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("fake")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake"})")}; constexpr int responseDelay = 1; mockHttpSessionAdapter->enqueueResponse(response, responseDelay); @@ -491,9 +537,9 @@ TEST_F(ConfigCatClientTest, Concurrency_DoNotStartNewFetchIfThereIsAnOngoingFetc #ifndef __APPLE__ // TODO: This test is broken on GitHub macos-latest os. TEST_F(ConfigCatClientTest, Concurrency_OngoingFetchDoesNotBlockGetValue) { - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("fake")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, string_format(kTestJsonFormat, R"("fake2")")}; + configcat::Response secondResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"fake2"})")}; constexpr int responseDelay = 3; mockHttpSessionAdapter->enqueueResponse(secondResponse, responseDelay); @@ -547,7 +593,7 @@ TEST_F(ConfigCatClientTest, GetValueTypeTest) { stringValue = client->getValue("", string("str")); EXPECT_EQ(stringValue, "str"); - int intValue = client->getValue("", 42); + int32_t intValue = client->getValue("", 42); EXPECT_EQ(intValue, 42); double doubleValue = client->getValue("", 42.0); @@ -557,16 +603,16 @@ TEST_F(ConfigCatClientTest, GetValueTypeTest) { TEST_F(ConfigCatClientTest, GetValueWithKeyNotFound) { SetUp(); - configcat::Response response = {200, string_format(kTestJsonFormat, "43")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::Int, R"({"i":43})")}; mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); auto value = client->getValue("nonexisting", 10); EXPECT_EQ(10, value); - ConfigCatUser* user = nullptr; - shared_ptr valuePtr = client->getValue("nonexisting", user); - EXPECT_EQ(nullptr, valuePtr); + std::shared_ptr user = nullptr; + auto settingValue = client->getValue("nonexisting", user); + EXPECT_FALSE(settingValue.has_value()); } TEST_F(ConfigCatClientTest, DefaultUserGetValue) { @@ -581,7 +627,7 @@ TEST_F(ConfigCatClientTest, DefaultUserGetValue) { client->setDefaultUser(user1); EXPECT_EQ("fake1", client->getValue("testStringKey", "")); - EXPECT_EQ("fake2", client->getValue("testStringKey", "", user2.get())); + EXPECT_EQ("fake2", client->getValue("testStringKey", "", user2)); client->clearDefaultUser(); EXPECT_EQ("testValue", client->getValue("testStringKey", "")); @@ -602,16 +648,16 @@ TEST_F(ConfigCatClientTest, DefaultUserGetAllValues) { EXPECT_EQ(6, allValues.size()); EXPECT_EQ(true, get(allValues["testBoolKey"])); EXPECT_EQ("fake1", get(allValues["testStringKey"])); - EXPECT_EQ(1, get(allValues["testIntKey"])); + EXPECT_EQ(1, get(allValues["testIntKey"])); EXPECT_EQ(1.1, get(allValues["testDoubleKey"])); EXPECT_TRUE(get(allValues["key1"])); EXPECT_FALSE(get(allValues["key2"])); - allValues = client->getAllValues(user2.get()); + allValues = client->getAllValues(user2); EXPECT_EQ(6, allValues.size()); EXPECT_EQ(true, get(allValues["testBoolKey"])); EXPECT_EQ("fake2", get(allValues["testStringKey"])); - EXPECT_EQ(1, get(allValues["testIntKey"])); + EXPECT_EQ(1, get(allValues["testIntKey"])); EXPECT_EQ(1.1, get(allValues["testDoubleKey"])); EXPECT_TRUE(get(allValues["key1"])); EXPECT_FALSE(get(allValues["key2"])); @@ -621,7 +667,7 @@ TEST_F(ConfigCatClientTest, DefaultUserGetAllValues) { EXPECT_EQ(6, allValues.size()); EXPECT_EQ(true, get(allValues["testBoolKey"])); EXPECT_EQ("testValue", get(allValues["testStringKey"])); - EXPECT_EQ(1, get(allValues["testIntKey"])); + EXPECT_EQ(1, get(allValues["testIntKey"])); EXPECT_EQ(1.1, get(allValues["testDoubleKey"])); EXPECT_TRUE(get(allValues["key1"])); EXPECT_FALSE(get(allValues["key2"])); @@ -680,3 +726,98 @@ TEST_F(ConfigCatClientTest, InitOffline) { EXPECT_EQ(1, mockHttpSessionAdapter->requests.size()); } +TEST_F(ConfigCatClientTest, ForceRefreshAfterClose) { + SetUp(); + + configcat::Response response = {200, kTestJsonString}; + mockHttpSessionAdapter->enqueueResponse(response); + ConfigCatClient::close(client); + + auto refreshResult = client->forceRefresh(); + + EXPECT_FALSE(refreshResult.success()); + EXPECT_TRUE(refreshResult.errorMessage.has_value()); + EXPECT_TRUE(refreshResult.errorMessage->find("has been closed") != string::npos); + EXPECT_TRUE(refreshResult.errorException == nullptr); +} + +TEST_F(ConfigCatClientTest, GetValueDetailsAfterClose) { + SetUp(); + + configcat::Response response = {200, kTestJsonString}; + mockHttpSessionAdapter->enqueueResponse(response); + client->forceRefresh(); + ConfigCatClient::close(client); + + auto user = make_shared("test@test1.com"); + auto details = client->getValueDetails("testStringKey", "", user); + + EXPECT_EQ("", details.value); + EXPECT_EQ("testStringKey", details.key); + EXPECT_TRUE(details.variationId == nullopt); + EXPECT_TRUE(details.isDefaultValue); + EXPECT_TRUE(details.errorMessage.has_value()); + EXPECT_TRUE(details.matchedTargetingRule == std::nullopt); + EXPECT_TRUE(details.matchedPercentageOption == std::nullopt); +} + +TEST_F(ConfigCatClientTest, SetOnlineAfterClose) { + SetUp(); + + configcat::Response response = {200, kTestJsonString}; + mockHttpSessionAdapter->enqueueResponse(response); + + EXPECT_FALSE(client->isOffline()); + ConfigCatClient::close(client); + + client->setOnline(); + EXPECT_TRUE(client->isOffline()); +} + +TEST_F(ConfigCatClientTest, ForceRefreshAfterCloseAll) { + SetUp(); + + configcat::Response response = {200, kTestJsonString}; + mockHttpSessionAdapter->enqueueResponse(response); + ConfigCatClient::closeAll(); + + auto refreshResult = client->forceRefresh(); + + EXPECT_FALSE(refreshResult.success()); + EXPECT_TRUE(refreshResult.errorMessage.has_value()); + EXPECT_TRUE(refreshResult.errorMessage->find("has been closed") != string::npos); + EXPECT_TRUE(refreshResult.errorException == nullptr); +} + +TEST_F(ConfigCatClientTest, GetValueDetailsAfterCloseAll) { + SetUp(); + + configcat::Response response = {200, kTestJsonString}; + mockHttpSessionAdapter->enqueueResponse(response); + client->forceRefresh(); + ConfigCatClient::closeAll(); + + auto user = make_shared("test@test1.com"); + auto details = client->getValueDetails("testStringKey", "", user); + + EXPECT_EQ("", details.value); + EXPECT_EQ("testStringKey", details.key); + EXPECT_TRUE(details.variationId == nullopt); + EXPECT_TRUE(details.isDefaultValue); + EXPECT_TRUE(details.errorMessage.has_value()); + EXPECT_TRUE(details.matchedTargetingRule == std::nullopt); + EXPECT_TRUE(details.matchedPercentageOption == std::nullopt); +} + +TEST_F(ConfigCatClientTest, SetOnlineAfterCloseAll) { + SetUp(); + + configcat::Response response = {200, kTestJsonString}; + mockHttpSessionAdapter->enqueueResponse(response); + + EXPECT_FALSE(client->isOffline()); + ConfigCatClient::closeAll(); + + client->setOnline(); + EXPECT_TRUE(client->isOffline()); +} \ No newline at end of file diff --git a/test/test-configcatclientintegration.cpp b/test/test-configcatclientintegration.cpp index 14465f5..8880226 100644 --- a/test/test-configcatclientintegration.cpp +++ b/test/test-configcatclientintegration.cpp @@ -1,7 +1,6 @@ #include #include "mock.h" #include "configcat/configcatclient.h" -#include "utils.h" using namespace configcat; using namespace std; @@ -29,7 +28,7 @@ TEST(ConfigCatClientIntegrationTest, DISABLED_ProxyTest) { * docker run -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta * * How to test the proxy server: - * curl --proxy localhost:3128 https://cdn-global.configcat.com/configuration-files/PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A/config_v5.json + * curl --proxy localhost:3128 https://cdn-global.configcat.com/configuration-files/PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A/config_v6.json * */ ConfigCatOptions options; diff --git a/test/test-configcatuser.cpp b/test/test-configcatuser.cpp index 0cca309..0c1afa5 100644 --- a/test/test-configcatuser.cpp +++ b/test/test-configcatuser.cpp @@ -16,14 +16,14 @@ TEST(ConfigCatUserTest, UserAttributesCaseInsensitivity) { } ); - EXPECT_EQ("id", user.identifier); - EXPECT_EQ("email", *user.getAttribute("Email")); + EXPECT_EQ("id", user.getIdentifier()); + EXPECT_EQ("email", get(*user.getAttribute("Email"))); EXPECT_EQ(nullptr, user.getAttribute("EMAIL")); EXPECT_EQ(nullptr, user.getAttribute("email")); - EXPECT_EQ("country", *user.getAttribute("Country")); + EXPECT_EQ("country", get(*user.getAttribute("Country"))); EXPECT_EQ(nullptr, user.getAttribute("COUNTRY")); EXPECT_EQ(nullptr, user.getAttribute("country")); - EXPECT_EQ("test", *user.getAttribute("custom")); + EXPECT_EQ("test", get(*user.getAttribute("custom"))); EXPECT_EQ(nullptr, user.getAttribute("not-existing")); } @@ -32,15 +32,22 @@ TEST(ConfigCatUserTest, ToJson) { "id", "email", "country", { - {"custom", "test"} + {"string", "test"}, + {"datetime", make_datetime(2023, 9, 19, 11, 1, 35, 999)}, + {"int", 42}, + {"double", 3.14} } ); json userJson = json::parse(user.toJson()); + string d = userJson["datetime"]; EXPECT_EQ("id", userJson["Identifier"]); EXPECT_EQ("email", userJson["Email"]); EXPECT_EQ("country", userJson["Country"]); - EXPECT_EQ("test", userJson["custom"]); + EXPECT_EQ("test", userJson["string"]); + EXPECT_EQ(42, userJson["int"]); + EXPECT_EQ(3.14, userJson["double"]); + EXPECT_EQ("2023-09-19T11:01:35.999Z"s, userJson["datetime"]); } diff --git a/test/test-configfetcher.cpp b/test/test-configfetcher.cpp index 839c206..9e7c216 100644 --- a/test/test-configfetcher.cpp +++ b/test/test-configfetcher.cpp @@ -1,10 +1,9 @@ #include #include "configfetcher.h" #include "configcat/configcatoptions.h" -#include "configcat/configcatlogger.h" +#include "configcatlogger.h" #include "configcat/consolelogger.h" #include "mock.h" -#include "utils.h" using namespace configcat; using namespace std; @@ -13,7 +12,7 @@ class ConfigFetcherTest : public ::testing::Test { public: static constexpr char kTestSdkKey[] = "TestSdkKey"; static constexpr char kCustomCdnUrl[] = "https://custom-cdn.configcat.com"; - static constexpr char kTestJson[] = R"({ "f": { "fakeKey": { "v": "fakeValue", "p": [], "r": [] } } })"; + static constexpr char kTestJson[] = R"({"f":{"fakeKey":{"t":1,"v":{"s":"fakeValue"}}}})"; unique_ptr fetcher; shared_ptr mockHttpSessionAdapter = make_shared(); shared_ptr logger = make_shared(make_shared(), make_shared()); @@ -31,22 +30,22 @@ class ConfigFetcherTest : public ::testing::Test { } string createTestJson(const string& url, RedirectMode redirectMode) { - return string_format(R"({ "p": { "u": "%s", "r": %d }, "f": {} })", url.c_str(), redirectMode); + return string_format(R"({"p":{"u":"%s","r":%d}})", url.c_str(), redirectMode); } }; TEST_F(ConfigFetcherTest, DataGovernance_ShouldStayOnGivenUrl) { SetUp(); - configcat::Response response = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, NoRedirect)}; + configcat::Response response = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, RedirectMode::No)}; mockHttpSessionAdapter->enqueueResponse(response); auto fetchResponse = fetcher->fetchConfiguration(); ASSERT_TRUE(fetchResponse.entry != ConfigEntry::empty); ASSERT_TRUE(fetchResponse.entry->config != Config::empty); auto& config = fetchResponse.entry->config; - EXPECT_EQ(config->preferences->url, ConfigFetcher::kGlobalBaseUrl); - EXPECT_EQ(config->preferences->redirect, NoRedirect); + EXPECT_EQ(config->preferences->baseUrl, ConfigFetcher::kGlobalBaseUrl); + EXPECT_EQ(config->preferences->redirectMode, RedirectMode::No); EXPECT_EQ(1, mockHttpSessionAdapter->requests.size()); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[0].url, ConfigFetcher::kGlobalBaseUrl)); } @@ -54,15 +53,15 @@ TEST_F(ConfigFetcherTest, DataGovernance_ShouldStayOnGivenUrl) { TEST_F(ConfigFetcherTest, DataGovernance_ShouldStayOnSameUrl) { SetUp(); - configcat::Response response = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, ShouldRedirect)}; + configcat::Response response = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, RedirectMode::Should)}; mockHttpSessionAdapter->enqueueResponse(response); auto fetchResponse = fetcher->fetchConfiguration(); ASSERT_TRUE(fetchResponse.entry != ConfigEntry::empty); ASSERT_TRUE(fetchResponse.entry->config != Config::empty); auto& config = fetchResponse.entry->config; - EXPECT_EQ(config->preferences->url, ConfigFetcher::kGlobalBaseUrl); - EXPECT_EQ(config->preferences->redirect, ShouldRedirect); + EXPECT_EQ(config->preferences->baseUrl, ConfigFetcher::kGlobalBaseUrl); + EXPECT_EQ(config->preferences->redirectMode, RedirectMode::Should); EXPECT_EQ(1, mockHttpSessionAdapter->requests.size()); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[0].url, ConfigFetcher::kGlobalBaseUrl)); } @@ -70,15 +69,15 @@ TEST_F(ConfigFetcherTest, DataGovernance_ShouldStayOnSameUrl) { TEST_F(ConfigFetcherTest, DataGovernance_ShouldStayOnSameUrlEvenWithForce) { SetUp(); - configcat::Response response = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, ForceRedirect)}; + configcat::Response response = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, RedirectMode::Force)}; mockHttpSessionAdapter->enqueueResponse(response); auto fetchResponse = fetcher->fetchConfiguration(); ASSERT_TRUE(fetchResponse.entry != ConfigEntry::empty); ASSERT_TRUE(fetchResponse.entry->config != Config::empty); auto& config = fetchResponse.entry->config; - EXPECT_EQ(config->preferences->url, ConfigFetcher::kGlobalBaseUrl); - EXPECT_EQ(config->preferences->redirect, ForceRedirect); + EXPECT_EQ(config->preferences->baseUrl, ConfigFetcher::kGlobalBaseUrl); + EXPECT_EQ(config->preferences->redirectMode, RedirectMode::Force); EXPECT_EQ(1, mockHttpSessionAdapter->requests.size()); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[0].url, ConfigFetcher::kGlobalBaseUrl)); } @@ -86,17 +85,17 @@ TEST_F(ConfigFetcherTest, DataGovernance_ShouldStayOnSameUrlEvenWithForce) { TEST_F(ConfigFetcherTest, DataGovernance_ShouldRedirectToAnotherServer) { SetUp(); - configcat::Response firstResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, ShouldRedirect)}; + configcat::Response firstResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, RedirectMode::Should)}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, NoRedirect)}; + configcat::Response secondResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, RedirectMode::No)}; mockHttpSessionAdapter->enqueueResponse(secondResponse); auto fetchResponse = fetcher->fetchConfiguration(); ASSERT_TRUE(fetchResponse.entry != ConfigEntry::empty); ASSERT_TRUE(fetchResponse.entry->config != Config::empty); auto& config = fetchResponse.entry->config; - EXPECT_EQ(config->preferences->url, ConfigFetcher::kEuOnlyBaseUrl); - EXPECT_EQ(config->preferences->redirect, NoRedirect); + EXPECT_EQ(config->preferences->baseUrl, ConfigFetcher::kEuOnlyBaseUrl); + EXPECT_EQ(config->preferences->redirectMode, RedirectMode::No); EXPECT_EQ(2, mockHttpSessionAdapter->requests.size()); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[0].url, ConfigFetcher::kGlobalBaseUrl)); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[1].url, ConfigFetcher::kEuOnlyBaseUrl)); @@ -105,17 +104,17 @@ TEST_F(ConfigFetcherTest, DataGovernance_ShouldRedirectToAnotherServer) { TEST_F(ConfigFetcherTest, DataGovernance_ShouldRedirectToAnotherServerWhenForced) { SetUp(); - configcat::Response firstResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, ForceRedirect)}; + configcat::Response firstResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, RedirectMode::Force)}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, NoRedirect)}; + configcat::Response secondResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, RedirectMode::No)}; mockHttpSessionAdapter->enqueueResponse(secondResponse); auto fetchResponse = fetcher->fetchConfiguration(); ASSERT_TRUE(fetchResponse.entry != ConfigEntry::empty); ASSERT_TRUE(fetchResponse.entry->config != Config::empty); auto& config = fetchResponse.entry->config; - EXPECT_EQ(config->preferences->url, ConfigFetcher::kEuOnlyBaseUrl); - EXPECT_EQ(config->preferences->redirect, NoRedirect); + EXPECT_EQ(config->preferences->baseUrl, ConfigFetcher::kEuOnlyBaseUrl); + EXPECT_EQ(config->preferences->redirectMode, RedirectMode::No); EXPECT_EQ(2, mockHttpSessionAdapter->requests.size()); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[0].url, ConfigFetcher::kGlobalBaseUrl)); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[1].url, ConfigFetcher::kEuOnlyBaseUrl)); @@ -124,8 +123,8 @@ TEST_F(ConfigFetcherTest, DataGovernance_ShouldRedirectToAnotherServerWhenForced TEST_F(ConfigFetcherTest, DataGovernance_ShouldBreakRedirectLoop) { SetUp(); - configcat::Response firstResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, ShouldRedirect)}; - configcat::Response secondResponse = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, ShouldRedirect)}; + configcat::Response firstResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, RedirectMode::Should)}; + configcat::Response secondResponse = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, RedirectMode::Should)}; mockHttpSessionAdapter->enqueueResponse(firstResponse); mockHttpSessionAdapter->enqueueResponse(secondResponse); mockHttpSessionAdapter->enqueueResponse(firstResponse); @@ -134,8 +133,8 @@ TEST_F(ConfigFetcherTest, DataGovernance_ShouldBreakRedirectLoop) { ASSERT_TRUE(fetchResponse.entry != ConfigEntry::empty); ASSERT_TRUE(fetchResponse.entry->config != Config::empty); auto& config = fetchResponse.entry->config; - EXPECT_EQ(config->preferences->url, ConfigFetcher::kEuOnlyBaseUrl); - EXPECT_EQ(config->preferences->redirect, ShouldRedirect); + EXPECT_EQ(config->preferences->baseUrl, ConfigFetcher::kEuOnlyBaseUrl); + EXPECT_EQ(config->preferences->redirectMode, RedirectMode::Should); EXPECT_EQ(3, mockHttpSessionAdapter->requests.size()); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[0].url, ConfigFetcher::kGlobalBaseUrl)); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[1].url, ConfigFetcher::kEuOnlyBaseUrl)); @@ -145,8 +144,8 @@ TEST_F(ConfigFetcherTest, DataGovernance_ShouldBreakRedirectLoop) { TEST_F(ConfigFetcherTest, DataGovernance_ShouldBreakRedirectLoopWhenForced) { SetUp(); - configcat::Response firstResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, ForceRedirect)}; - configcat::Response secondResponse = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, ForceRedirect)}; + configcat::Response firstResponse = {200, createTestJson(ConfigFetcher::kEuOnlyBaseUrl, RedirectMode::Force)}; + configcat::Response secondResponse = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, RedirectMode::Force)}; mockHttpSessionAdapter->enqueueResponse(firstResponse); mockHttpSessionAdapter->enqueueResponse(secondResponse); mockHttpSessionAdapter->enqueueResponse(firstResponse); @@ -155,8 +154,8 @@ TEST_F(ConfigFetcherTest, DataGovernance_ShouldBreakRedirectLoopWhenForced) { ASSERT_TRUE(fetchResponse.entry != ConfigEntry::empty); ASSERT_TRUE(fetchResponse.entry->config != Config::empty); auto& config = fetchResponse.entry->config; - EXPECT_EQ(config->preferences->url, ConfigFetcher::kEuOnlyBaseUrl); - EXPECT_EQ(config->preferences->redirect, ForceRedirect); + EXPECT_EQ(config->preferences->baseUrl, ConfigFetcher::kEuOnlyBaseUrl); + EXPECT_EQ(config->preferences->redirectMode, RedirectMode::Force); EXPECT_EQ(3, mockHttpSessionAdapter->requests.size()); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[0].url, ConfigFetcher::kGlobalBaseUrl)); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[1].url, ConfigFetcher::kEuOnlyBaseUrl)); @@ -166,15 +165,15 @@ TEST_F(ConfigFetcherTest, DataGovernance_ShouldBreakRedirectLoopWhenForced) { TEST_F(ConfigFetcherTest, DataGovernance_ShouldRespectCustomUrl) { SetUp(kCustomCdnUrl); - configcat::Response response = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, ShouldRedirect)}; + configcat::Response response = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, RedirectMode::Should)}; mockHttpSessionAdapter->enqueueResponse(response); auto fetchResponse = fetcher->fetchConfiguration(); ASSERT_TRUE(fetchResponse.entry != ConfigEntry::empty); ASSERT_TRUE(fetchResponse.entry->config != Config::empty); auto& config = fetchResponse.entry->config; - EXPECT_EQ(config->preferences->url, ConfigFetcher::kGlobalBaseUrl); - EXPECT_EQ(config->preferences->redirect, ShouldRedirect); + EXPECT_EQ(config->preferences->baseUrl, ConfigFetcher::kGlobalBaseUrl); + EXPECT_EQ(config->preferences->redirectMode, RedirectMode::Should); EXPECT_EQ(1, mockHttpSessionAdapter->requests.size()); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[0].url, kCustomCdnUrl)); } @@ -182,8 +181,8 @@ TEST_F(ConfigFetcherTest, DataGovernance_ShouldRespectCustomUrl) { TEST_F(ConfigFetcherTest, DataGovernance_ShouldNotRespectCustomUrlWhenForced) { SetUp(kCustomCdnUrl); - configcat::Response firstResponse = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, ForceRedirect)}; - configcat::Response secondResponse = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, NoRedirect)}; + configcat::Response firstResponse = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, RedirectMode::Force)}; + configcat::Response secondResponse = {200, createTestJson(ConfigFetcher::kGlobalBaseUrl, RedirectMode::No)}; mockHttpSessionAdapter->enqueueResponse(firstResponse); mockHttpSessionAdapter->enqueueResponse(secondResponse); auto fetchResponse = fetcher->fetchConfiguration(); @@ -191,8 +190,8 @@ TEST_F(ConfigFetcherTest, DataGovernance_ShouldNotRespectCustomUrlWhenForced) { ASSERT_TRUE(fetchResponse.entry != ConfigEntry::empty); ASSERT_TRUE(fetchResponse.entry->config != Config::empty); auto& config = fetchResponse.entry->config; - EXPECT_EQ(config->preferences->url, ConfigFetcher::kGlobalBaseUrl); - EXPECT_EQ(config->preferences->redirect, NoRedirect); + EXPECT_EQ(config->preferences->baseUrl, ConfigFetcher::kGlobalBaseUrl); + EXPECT_EQ(config->preferences->redirectMode, RedirectMode::No); EXPECT_EQ(2, mockHttpSessionAdapter->requests.size()); EXPECT_TRUE(starts_with(mockHttpSessionAdapter->requests[0].url, kCustomCdnUrl)); @@ -210,7 +209,7 @@ TEST_F(ConfigFetcherTest, Fetcher_SimpleFetchSuccess) { EXPECT_TRUE(fetchResponse.isFetched()); EXPECT_TRUE(fetchResponse.entry != nullptr && fetchResponse.entry != ConfigEntry::empty); - auto entries = *fetchResponse.entry->config->entries; + auto entries = *fetchResponse.entry->config->settings; EXPECT_EQ("fakeValue", get(entries["fakeKey"].value)); } @@ -259,7 +258,7 @@ TEST_F(ConfigFetcherTest, Fetcher_FetchNotModifiedEtag) { SetUp(); auto eTag = "test"; - configcat::Response firstResponse = {200, kTestJson, {{"ETag", eTag}}}; + configcat::Response firstResponse = {200, kTestJson, {{"etag", eTag}}}; mockHttpSessionAdapter->enqueueResponse(firstResponse); configcat::Response secondResponse = {304, ""}; mockHttpSessionAdapter->enqueueResponse(secondResponse); @@ -269,7 +268,7 @@ TEST_F(ConfigFetcherTest, Fetcher_FetchNotModifiedEtag) { EXPECT_TRUE(fetchResponse.isFetched()); EXPECT_TRUE(fetchResponse.entry != nullptr && fetchResponse.entry != ConfigEntry::empty); EXPECT_EQ(eTag, fetchResponse.entry->eTag); - auto entries = *fetchResponse.entry->config->entries; + auto entries = *fetchResponse.entry->config->settings; EXPECT_EQ("fakeValue", get(entries["fakeKey"].value)); fetchResponse = fetcher->fetchConfiguration(eTag); diff --git a/test/test-evaluation.cpp b/test/test-evaluation.cpp new file mode 100644 index 0000000..67039c6 --- /dev/null +++ b/test/test-evaluation.cpp @@ -0,0 +1,563 @@ +#include +#include +#include +#include +#include "configcat/configcatuser.h" +#include "configcat/configcatclient.h" +#include "configcatlogger.h" +#include "test.h" +#include "mock.h" +#include "configcat/log.h" +#include "configcat/fileoverridedatasource.h" +#include "configcat/mapoverridedatasource.h" +#include "rolloutevaluator.h" + +using namespace std; +using namespace configcat; + +class EvaluationTest : public ::testing::Test { +public: + static const string directoryPath; +}; +const string EvaluationTest::directoryPath = RemoveFileName(__FILE__); + +class ComparisonAttributeConversionToCanonicalStringTestSuite : public ::testing::TestWithParam> {}; +INSTANTIATE_TEST_SUITE_P(EvaluationTest, ComparisonAttributeConversionToCanonicalStringTestSuite, ::testing::Values( + make_tuple("numberToStringConversion", .12345, "1"), + make_tuple("numberToStringConversionInt", 125.0, "4"), + make_tuple("numberToStringConversionPositiveExp", -1.23456789e96, "2"), + make_tuple("numberToStringConversionNegativeExp", -12345.6789E-100, "4"), + make_tuple("numberToStringConversionNaN", NAN, "3"), + make_tuple("numberToStringConversionPositiveInf", INFINITY, "4"), + make_tuple("numberToStringConversionNegativeInf", -INFINITY, "3"), + make_tuple("dateToStringConversion", make_datetime(2023, 3, 31, 23, 59, 59, 999), "3"), + make_tuple("dateToStringConversion", 1680307199.999, "3"), // Assuming this needs conversion to date + make_tuple("dateToStringConversionNaN", NAN, "3"), + make_tuple("dateToStringConversionPositiveInf", INFINITY, "1"), + make_tuple("dateToStringConversionNegativeInf", -INFINITY, "5"), + make_tuple("stringArrayToStringConversion", vector{ "read", "Write", " eXecute " }, "4"), + make_tuple("stringArrayToStringConversionEmpty", vector{}, "5"), + make_tuple("stringArrayToStringConversionSpecialChars", vector{"+<>%\"'\\/\t\r\n"}, "3"), + make_tuple("stringArrayToStringConversionUnicode", vector{"äöüÄÖÜçéèñışğ⢙✓😀"}, "2") +)); +TEST_P(ComparisonAttributeConversionToCanonicalStringTestSuite, ComparisonAttributeConversionToCanonicalString) { + auto [key, customAttributeValue, expectedReturnValue] = GetParam(); + + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + options.flagOverrides = make_shared(EvaluationTest::directoryPath + "data/comparison_attribute_conversion.json", LocalOnly); + auto client = ConfigCatClient::get("local-only", &options); + + std::unordered_map custom = { {"Custom1", customAttributeValue} }; + auto user = make_shared("12345", nullopt, nullopt, custom); + + auto result = client->getValue(key, "default", user); + + EXPECT_EQ(expectedReturnValue, result); + + ConfigCatClient::closeAll(); +} + +class ComparisonAttributeTrimmingTestSuite : public ::testing::TestWithParam> {}; +INSTANTIATE_TEST_SUITE_P(EvaluationTest, ComparisonAttributeTrimmingTestSuite, ::testing::Values( + make_tuple("isoneof", "no trim"), + make_tuple("isnotoneof", "no trim"), + make_tuple("isoneofhashed", "no trim"), + make_tuple("isnotoneofhashed", "no trim"), + make_tuple("equalshashed", "no trim"), + make_tuple("notequalshashed", "no trim"), + make_tuple("arraycontainsanyofhashed", "no trim"), + make_tuple("arraynotcontainsanyofhashed", "no trim"), + make_tuple("equals", "no trim"), + make_tuple("notequals", "no trim"), + make_tuple("startwithanyof", "no trim"), + make_tuple("notstartwithanyof", "no trim"), + make_tuple("endswithanyof", "no trim"), + make_tuple("notendswithanyof", "no trim"), + make_tuple("arraycontainsanyof", "no trim"), + make_tuple("arraynotcontainsanyof", "no trim"), + make_tuple("startwithanyofhashed", "no trim"), + make_tuple("notstartwithanyofhashed", "no trim"), + make_tuple("endswithanyofhashed", "no trim"), + make_tuple("notendswithanyofhashed", "no trim"), + // semver comparators user values trimmed because of backward compatibility + make_tuple("semverisoneof", "4 trim"), + make_tuple("semverisnotoneof", "5 trim"), + make_tuple("semverless", "6 trim"), + make_tuple("semverlessequals", "7 trim"), + make_tuple("semvergreater", "8 trim"), + make_tuple("semvergreaterequals", "9 trim"), + // number and date comparators user values trimmed because of backward compatibility + make_tuple("numberequals", "10 trim"), + make_tuple("numbernotequals", "11 trim"), + make_tuple("numberless", "12 trim"), + make_tuple("numberlessequals", "13 trim"), + make_tuple("numbergreater", "14 trim"), + make_tuple("numbergreaterequals", "15 trim"), + make_tuple("datebefore", "18 trim"), + make_tuple("dateafter", "19 trim"), + // "contains any of" and "not contains any of" is a special case, the not trimmed user attribute checked against not trimmed comparator values. + make_tuple("containsanyof", "no trim"), + make_tuple("notcontainsanyof", "no trim") +)); +TEST_P(ComparisonAttributeTrimmingTestSuite, ComparisonAttributeTrimming) { + auto [key, expectedReturnValue] = GetParam(); + + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + options.flagOverrides = make_shared(EvaluationTest::directoryPath + "data/comparison_attribute_trimming.json", LocalOnly); + auto client = ConfigCatClient::get("local-only", &options); + + std::unordered_map custom = { + {"Version", " 1.0.0 "}, + {"Number", " 3 "}, + {"Date", " 1705253400 "} + }; + auto user = make_shared(" 12345 ", nullopt, "[\" USA \"]", custom); + + auto result = client->getValue(key, "default", user); + + EXPECT_EQ(expectedReturnValue, result); + + ConfigCatClient::closeAll(); +} + +class ComparisonValueTrimmingTestSuite : public ::testing::TestWithParam> {}; +INSTANTIATE_TEST_SUITE_P(EvaluationTest, ComparisonValueTrimmingTestSuite, ::testing::Values( + make_tuple("isoneof", "no trim"), + make_tuple("isnotoneof", "no trim"), + make_tuple("containsanyof", "no trim"), + make_tuple("notcontainsanyof", "no trim"), + make_tuple("isoneofhashed", "no trim"), + make_tuple("isnotoneofhashed", "no trim"), + make_tuple("equalshashed", "no trim"), + make_tuple("notequalshashed", "no trim"), + make_tuple("arraycontainsanyofhashed", "no trim"), + make_tuple("arraynotcontainsanyofhashed", "no trim"), + make_tuple("equals", "no trim"), + make_tuple("notequals", "no trim"), + make_tuple("startwithanyof", "no trim"), + make_tuple("notstartwithanyof", "no trim"), + make_tuple("endswithanyof", "no trim"), + make_tuple("notendswithanyof", "no trim"), + make_tuple("arraycontainsanyof", "no trim"), + make_tuple("arraynotcontainsanyof", "no trim"), + make_tuple("startwithanyofhashed", "no trim"), + make_tuple("notstartwithanyofhashed", "no trim"), + make_tuple("endswithanyofhashed", "no trim"), + make_tuple("notendswithanyofhashed", "no trim"), + // semver comparator values trimmed because of backward compatibility + make_tuple("semverisoneof", "4 trim"), + make_tuple("semverisnotoneof", "5 trim"), + make_tuple("semverless", "6 trim"), + make_tuple("semverlessequals", "7 trim"), + make_tuple("semvergreater", "8 trim"), + make_tuple("semvergreaterequals", "9 trim") +)); +TEST_P(ComparisonValueTrimmingTestSuite, ComparisonValueTrimming) { + auto [key, expectedReturnValue] = GetParam(); + + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + options.flagOverrides = make_shared(EvaluationTest::directoryPath + "data/comparison_value_trimming.json", LocalOnly); + auto client = ConfigCatClient::get("local-only", &options); + + std::unordered_map custom = { + {"Version", "1.0.0"}, + {"Number", "3"}, + {"Date", "1705253400"} + }; + auto user = make_shared("12345", nullopt, "[\"USA\"]", custom); + + auto result = client->getValue(key, "default", user); + + EXPECT_EQ(expectedReturnValue, result); + + ConfigCatClient::closeAll(); +} + +TEST_F(EvaluationTest, UserObjectAttributeValueConversionTextComparisons) { + std::shared_ptr testLogger = make_shared(LOG_LEVEL_WARNING); + + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + options.logger = testLogger; + auto client = ConfigCatClient::get("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", &options); + client->forceRefresh(); + + string key = "boolTextEqualsNumber"; + string customAttributeName = "Custom1"; + int customAttributeValue = 42; + auto user = ConfigCatUser::create("12345", nullopt, nullopt, { { customAttributeName, customAttributeValue } }); + + auto result = client->getValue(key, false, user); + EXPECT_TRUE(result); + + auto expectedLog = string_format("WARNING [3005] Evaluation of condition (User.%s EQUALS '%s') for setting '%s' may not produce the expected" + " result (the User.%s attribute is not a string value, thus it was automatically converted to the " + "string value '%s'). Please make sure that using a non-string value was intended.\n", + customAttributeName.c_str(), number_to_string(customAttributeValue).c_str(), + key.c_str(), customAttributeName.c_str(), number_to_string(customAttributeValue).c_str()); + EXPECT_EQ(expectedLog, testLogger->text); + + ConfigCatClient::closeAll(); +} + +class UserObjectAttributeValueConversion_NonTextComparisonsTestSuite : public ::testing::TestWithParam> {}; +INSTANTIATE_TEST_SUITE_P(EvaluationTest, UserObjectAttributeValueConversion_NonTextComparisonsTestSuite, ::testing::Values( + // SemVer-based comparisons + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "0.0", "20%"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "0.9.9", "< 1.0.0"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "1.0.0", "20%"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", "1.1", "20%"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 0, "20%"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 0.9, "20%"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", "lessThanWithPercentage", "12345", "Custom1", 2, "20%"), + // Number-based comparisons + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -INFINITY, "<2.1"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", -1, "<2.1"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2, "<2.1"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 2.1, "<=2,1"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 3, "<>4.2"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", 5, ">=5"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", INFINITY, ">5"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", NAN, "<>4.2"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-Infinity", "<2.1"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", " -Infinity ", "<2.1"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "-1", "<2.1"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2", "<2.1"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2.1", "<=2,1"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "2,1", "<=2,1"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "3", "<>4.2"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "5", ">=5"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "Infinity", ">5"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", " Infinity ", ">5"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaN", "<>4.2"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", " NaN ", "<>4.2"), + make_tuple("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", "numberWithPercentage", "12345", "Custom1", "NaNa", "80%"), + // Date time-based comparisons + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", make_datetime(2023, 3, 31, 23, 59, 59, 999), false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", make_datetime(2023, 4, 1, 0, 0, 0, 1), true), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", make_datetime(2023, 4, 30, 23, 59, 59, 999), true), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", make_datetime(2023, 5, 1, 0, 0, 0, 1), false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", -INFINITY, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307199.999, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307200.001, true), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899199.999, true), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899200.001, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", INFINITY, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", NAN, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307199, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1680307201, true), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899199, true), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", 1682899201, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "-Infinity", false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", " -Infinity ", false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307199.999", false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1680307200.001", true), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899199.999", true), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "1682899200.001", false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "+Infinity", false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", " +Infinity ", false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", "NaN", false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "boolTrueIn202304", "12345", "Custom1", " NaN ", false), + // String array-based comparisons + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", vector{"x", "read"}, "Dog"), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", vector{"x", "Read"}, "Cat"), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "[\"x\", \"read\"]", "Dog"), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "[\"x\", \"Read\"]", "Cat"), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", "stringArrayContainsAnyOfDogDefaultCat", "12345", "Custom1", "x, read", "Cat") +)); +TEST_P(UserObjectAttributeValueConversion_NonTextComparisonsTestSuite, UserObjectAttributeValueConversion_NonTextComparisons) { + auto [sdkKey, key, userId, customAttributeName, customAttributeValue, expectedReturnValue] = GetParam(); + + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + auto client = ConfigCatClient::get(sdkKey, &options); + client->forceRefresh(); + + std::unordered_map custom = { + {customAttributeName, customAttributeValue} + }; + auto user = make_shared(userId, nullopt, nullopt, custom); + + auto details = client->getValueDetails(key, user); + EXPECT_EQ(expectedReturnValue, details.value); + + ConfigCatClient::closeAll(); +} + +class PrerequisiteFlagCircularDependencyTestSuite : public ::testing::TestWithParam> {}; +INSTANTIATE_TEST_SUITE_P(EvaluationTest, PrerequisiteFlagCircularDependencyTestSuite, ::testing::Values( + make_tuple("key1", "'key1' -> 'key1'"), + make_tuple("key2", "'key2' -> 'key3' -> 'key2'"), + make_tuple("key4", "'key4' -> 'key3' -> 'key2' -> 'key3'") +)); +TEST_P(PrerequisiteFlagCircularDependencyTestSuite, PrerequisiteFlagCircularDependency) { + auto [key, dependencyCycle] = GetParam(); + + std::shared_ptr testLogger = make_shared(); + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + options.logger = testLogger; + options.flagOverrides = make_shared(EvaluationTest::directoryPath + "data/test_circulardependency_v6.json", LocalOnly); + auto client = ConfigCatClient::get("local-only", &options); + + auto details = client->getValueDetails(key); + + EXPECT_TRUE(details.isDefaultValue); + EXPECT_TRUE(details.value == nullopt); + EXPECT_TRUE(details.errorMessage != nullopt); + EXPECT_TRUE(details.errorException != nullptr); + auto exceptionMessage = unwrap_exception_message(details.errorException); + EXPECT_THAT(exceptionMessage, ::testing::HasSubstr("Circular dependency detected")); + EXPECT_THAT(exceptionMessage, ::testing::HasSubstr(dependencyCycle)); + EXPECT_THAT(testLogger->text, ::testing::HasSubstr("Circular dependency detected")); + EXPECT_THAT(testLogger->text, ::testing::HasSubstr(dependencyCycle)); + + ConfigCatClient::closeAll(); +} + +// https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb +class PrerequisiteFlagComparisonValueTypeMismatchTestSuite : public ::testing::TestWithParam>> {}; +INSTANTIATE_TEST_SUITE_P(EvaluationTest, PrerequisiteFlagComparisonValueTypeMismatchTestSuite, ::testing::Values( + make_tuple("stringDependsOnBool", "mainBoolFlag", true, "Dog"), + make_tuple("stringDependsOnBool", "mainBoolFlag", false, "Cat"), + make_tuple("stringDependsOnBool", "mainBoolFlag", "1", nullopt), + make_tuple("stringDependsOnBool", "mainBoolFlag", 1, nullopt), + make_tuple("stringDependsOnBool", "mainBoolFlag", 1.0, nullopt), + make_tuple("stringDependsOnString", "mainStringFlag", "private", "Dog"), + make_tuple("stringDependsOnString", "mainStringFlag", "Private", "Cat"), + make_tuple("stringDependsOnString", "mainStringFlag", true, nullopt), + make_tuple("stringDependsOnString", "mainStringFlag", 1, nullopt), + make_tuple("stringDependsOnString", "mainStringFlag", 1.0, nullopt), + make_tuple("stringDependsOnInt", "mainIntFlag", 2, "Dog"), + make_tuple("stringDependsOnInt", "mainIntFlag", 1, "Cat"), + make_tuple("stringDependsOnInt", "mainIntFlag", "2", nullopt), + make_tuple("stringDependsOnInt", "mainIntFlag", true, nullopt), + make_tuple("stringDependsOnInt", "mainIntFlag", 2.0, nullopt), + make_tuple("stringDependsOnDouble", "mainDoubleFlag", 0.1, "Dog"), + make_tuple("stringDependsOnDouble", "mainDoubleFlag", 0.11, "Cat"), + make_tuple("stringDependsOnDouble", "mainDoubleFlag", "0.1", nullopt), + make_tuple("stringDependsOnDouble", "mainDoubleFlag", true, nullopt), + make_tuple("stringDependsOnDouble", "mainDoubleFlag", 1, nullopt) +)); +TEST_P(PrerequisiteFlagComparisonValueTypeMismatchTestSuite, PrerequisiteFlagComparisonValueTypeMismatch) { + auto [key, prerequisiteFlagKey, prerequisiteFlagValue, expectedReturnValue] = GetParam(); + + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + options.flagOverrides = make_shared(std::unordered_map{{ prerequisiteFlagKey, prerequisiteFlagValue }}, LocalOverRemote); + auto client = ConfigCatClient::get("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg", &options); + client->forceRefresh(); + + auto details = client->getValueDetails(key); + + if (expectedReturnValue) { + EXPECT_FALSE(details.isDefaultValue); + EXPECT_EQ(expectedReturnValue, details.value); + EXPECT_TRUE(details.errorMessage == nullopt); + EXPECT_TRUE(details.errorException == nullptr); + } else { + EXPECT_TRUE(details.isDefaultValue); + EXPECT_TRUE(details.value == nullopt); + EXPECT_TRUE(details.errorMessage != nullopt); + EXPECT_TRUE(details.errorException != nullptr); + auto exceptionMessage = unwrap_exception_message(details.errorException); + EXPECT_THAT(exceptionMessage, testing::MatchesRegex("Type mismatch between comparison value '.+' and prerequisite flag '.+'.")); + }; + + ConfigCatClient::closeAll(); +} + +// https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb +class PrerequisiteFlagOverrideTestSuite : public ::testing::TestWithParam, std::optional>> {}; +INSTANTIATE_TEST_SUITE_P(EvaluationTest, PrerequisiteFlagOverrideTestSuite, ::testing::Values( + make_tuple("stringDependsOnString", "1", "john@sensitivecompany.com", nullopt, "Dog"), + make_tuple("stringDependsOnString", "1", "john@sensitivecompany.com", RemoteOverLocal, "Dog"), + make_tuple("stringDependsOnString", "1", "john@sensitivecompany.com", LocalOverRemote, "Dog"), + make_tuple("stringDependsOnString", "1", "john@sensitivecompany.com", LocalOnly, nullopt), + make_tuple("stringDependsOnString", "2", "john@notsensitivecompany.com", nullopt, "Cat"), + make_tuple("stringDependsOnString", "2", "john@notsensitivecompany.com", RemoteOverLocal, "Cat"), + make_tuple("stringDependsOnString", "2", "john@notsensitivecompany.com", LocalOverRemote, "Dog"), + make_tuple("stringDependsOnString", "2", "john@notsensitivecompany.com", LocalOnly, nullopt), + make_tuple("stringDependsOnInt", "1", "john@sensitivecompany.com", nullopt, "Dog"), + make_tuple("stringDependsOnInt", "1", "john@sensitivecompany.com", RemoteOverLocal, "Dog"), + make_tuple("stringDependsOnInt", "1", "john@sensitivecompany.com", LocalOverRemote, "Cat"), + make_tuple("stringDependsOnInt", "1", "john@sensitivecompany.com", LocalOnly, nullopt), + make_tuple("stringDependsOnInt", "2", "john@notsensitivecompany.com", nullopt, "Cat"), + make_tuple("stringDependsOnInt", "2", "john@notsensitivecompany.com", RemoteOverLocal, "Cat"), + make_tuple("stringDependsOnInt", "2", "john@notsensitivecompany.com", LocalOverRemote, "Dog"), + make_tuple("stringDependsOnInt", "2", "john@notsensitivecompany.com", LocalOnly, nullopt) +)); +TEST_P(PrerequisiteFlagOverrideTestSuite, PrerequisiteFlagOverride) { + auto [key, userId, email, overrideBehaviour, expectedReturnValue] = GetParam(); + + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + if (overrideBehaviour) { + options.flagOverrides = make_shared(EvaluationTest::directoryPath + "data/test_override_flagdependency_v6.json", *overrideBehaviour); + } + auto client = ConfigCatClient::get("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg", &options); + client->forceRefresh(); + + auto user = make_shared(userId, email); + auto details = client->getValueDetails(key, user); + + if (expectedReturnValue) { + EXPECT_FALSE(details.isDefaultValue); + EXPECT_EQ(expectedReturnValue, details.value); + EXPECT_TRUE(details.errorMessage == nullopt); + EXPECT_TRUE(details.errorException == nullptr); + } else { + EXPECT_TRUE(details.isDefaultValue); + EXPECT_TRUE(details.value == nullopt); + EXPECT_TRUE(details.errorMessage != nullopt); + }; + + ConfigCatClient::closeAll(); +} + +// https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb +class ConfigSaltAndSegmentsOverrideTestSuite : public ::testing::TestWithParam, std::optional>> {}; +INSTANTIATE_TEST_SUITE_P(EvaluationTest, ConfigSaltAndSegmentsOverrideTestSuite, ::testing::Values( + make_tuple("developerAndBetaUserSegment", "1", "john@example.com", nullopt, false), + make_tuple("developerAndBetaUserSegment", "1", "john@example.com", RemoteOverLocal, false), + make_tuple("developerAndBetaUserSegment", "1", "john@example.com", LocalOverRemote, true), + make_tuple("developerAndBetaUserSegment", "1", "john@example.com", LocalOnly, true), + make_tuple("notDeveloperAndNotBetaUserSegment", "2", "kate@example.com", nullopt, true), + make_tuple("notDeveloperAndNotBetaUserSegment", "2", "kate@example.com", RemoteOverLocal, true), + make_tuple("notDeveloperAndNotBetaUserSegment", "2", "kate@example.com", LocalOverRemote, true), + make_tuple("notDeveloperAndNotBetaUserSegment", "2", "kate@example.com", LocalOnly, nullopt) +)); +TEST_P(ConfigSaltAndSegmentsOverrideTestSuite, ConfigSaltAndSegmentsOverride) { + auto [key, userId, email, overrideBehaviour, expectedReturnValue] = GetParam(); + + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + if (overrideBehaviour) { + options.flagOverrides = make_shared(EvaluationTest::directoryPath + "data/test_override_segments_v6.json", *overrideBehaviour); + } + auto client = ConfigCatClient::get("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/h99HYXWWNE2bH8eWyLAVMA", &options); + client->forceRefresh(); + + auto user = make_shared(userId, email); + auto details = client->getValueDetails(key, user); + + if (expectedReturnValue) { + EXPECT_FALSE(details.isDefaultValue); + EXPECT_EQ(expectedReturnValue, details.value); + EXPECT_TRUE(details.errorMessage == nullopt); + EXPECT_TRUE(details.errorException == nullptr); + } else { + EXPECT_TRUE(details.isDefaultValue); + EXPECT_TRUE(details.value == nullopt); + EXPECT_TRUE(details.errorMessage != nullopt); + }; + + ConfigCatClient::closeAll(); +} + +// https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb +class EvaluationDetailsMatchedEvaluationRuleAndPercentageOptionTestSuite : public ::testing::TestWithParam, std::optional, std::optional, std::optional, bool, bool>> {}; +INSTANTIATE_TEST_SUITE_P(EvaluationTest, EvaluationDetailsMatchedEvaluationRuleAndPercentageOptionTestSuite, ::testing::Values( + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", nullopt, nullopt, nullopt, "Cat", false, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", nullopt, nullopt, "Cat", false, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@example.com", nullopt, "Dog", true, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", nullopt, "Cat", false, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", "", "Frog", true, true), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "a@configcat.com", "US", "Fish", true, true), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", nullopt, "Cat", false, false), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", "", "Falcon", false, true), + make_tuple("configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", "stringMatchedTargetingRuleAndOrPercentageOption", "12345", "b@configcat.com", "US", "Spider", false, true) +)); +TEST_P(EvaluationDetailsMatchedEvaluationRuleAndPercentageOptionTestSuite, EvaluationDetailsMatchedEvaluationRuleAndPercentageOption) { + auto [sdkKey, key, userId, email, percentageBase, expectedReturnValue, expectedMatchedTargetingRuleSet, expectedMatchedPercentageOptionSet] = GetParam(); + + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + auto client = ConfigCatClient::get(sdkKey, &options); + client->forceRefresh(); + + + std::unordered_map custom = {}; + if (percentageBase) + custom.insert({"PercentageBase", *percentageBase}); + auto user = (userId) ? make_shared(*userId, email, nullopt, custom) : nullptr; + auto details = client->getValueDetails(key, user); + + EXPECT_EQ(expectedReturnValue, details.value); + EXPECT_EQ(expectedMatchedTargetingRuleSet, details.matchedTargetingRule != nullopt); + EXPECT_EQ(expectedMatchedPercentageOptionSet, details.matchedPercentageOption != nullopt); + + ConfigCatClient::closeAll(); +} + +template +void checkValueAndDefaultValueTypeCompatibility(const ConfigCatClient& client, + std::shared_ptr& logger, + const std::string& key, + SettingType valueSettingType, + const ValueType& defaultValue, + SettingType defaultValueSettingType, + const Value& expectedReturnValue) { + logger->clear(); + EvaluationDetails details = client.getValueDetails(key, defaultValue); + + if (valueSettingType == defaultValueSettingType) { + EXPECT_FALSE(details.isDefaultValue); + EXPECT_EQ(expectedReturnValue, Value(details.value)); + EXPECT_EQ(nullopt, details.errorMessage); + EXPECT_EQ(nullptr, details.errorException); + } else { + EXPECT_TRUE(details.isDefaultValue); + EXPECT_EQ(expectedReturnValue, Value(details.value)); + EXPECT_THAT(logger->text, ::testing::HasSubstr("The type of a setting must match the type of the specified default value.")); + } +} + +// https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-1927-4d6b-8fb9-b1472564e2d3/244cf8b0-f604-11e8-b543-f23c917f9d8d +class ValueAndDefaultValueTypeCompatibilityTestSuite : public ::testing::TestWithParam> {}; +INSTANTIATE_TEST_SUITE_P(EvaluationTest, ValueAndDefaultValueTypeCompatibilityTestSuite, ::testing::Values( + make_tuple("boolDefaultTrue", SettingType::Boolean, false, true), + make_tuple("boolDefaultTrue", SettingType::Boolean, "", ""), + make_tuple("boolDefaultTrue", SettingType::Boolean, 0, 0), + make_tuple("boolDefaultTrue", SettingType::Boolean, 0.0, 0.0), + make_tuple("stringDefaultCat", SettingType::String, false, false), + make_tuple("stringDefaultCat", SettingType::String, "", "Cat"), + make_tuple("stringDefaultCat", SettingType::String, 0, 0), + make_tuple("stringDefaultCat", SettingType::String, 0.0, 0.0), + make_tuple("integerDefaultOne", SettingType::Int, false, false), + make_tuple("integerDefaultOne", SettingType::Int, "", ""), + make_tuple("integerDefaultOne", SettingType::Int, 0, 1), + make_tuple("integerDefaultOne", SettingType::Int, 0.0, 0.0), + make_tuple("doubleDefaultPi", SettingType::Double, false, false), + make_tuple("doubleDefaultPi", SettingType::Double, "", ""), + make_tuple("doubleDefaultPi", SettingType::Double, 0, 0), + make_tuple("doubleDefaultPi", SettingType::Double, 0.0, 3.1415) +)); +TEST_P(ValueAndDefaultValueTypeCompatibilityTestSuite, ValueAndDefaultValueTypeCompatibility) { + auto [key, valueSettingType, defaultValue, expectedReturnValue] = GetParam(); + + auto testLogger = make_shared(LOG_LEVEL_WARNING); + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + options.logger = testLogger; + auto client = ConfigCatClient::get("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/AG6C1ngVb0CvM07un6JisQ", &options); + client->forceRefresh(); + + auto defaultValueSettingType = static_cast(defaultValue).getSettingType(); + if (holds_alternative(defaultValue)) { + checkValueAndDefaultValueTypeCompatibility(*client, testLogger, key, valueSettingType, get(defaultValue), defaultValueSettingType, expectedReturnValue); + } else if (holds_alternative(defaultValue)) { + checkValueAndDefaultValueTypeCompatibility(*client, testLogger, key, valueSettingType, get(defaultValue), defaultValueSettingType, expectedReturnValue); + } else if (holds_alternative(defaultValue)) { + checkValueAndDefaultValueTypeCompatibility(*client, testLogger, key, valueSettingType, get(defaultValue), defaultValueSettingType, expectedReturnValue); + } else if (holds_alternative(defaultValue)) { + checkValueAndDefaultValueTypeCompatibility(*client, testLogger, key, valueSettingType, get(defaultValue), defaultValueSettingType, expectedReturnValue); + } else { + throw std::runtime_error("Return value type is invalid."); + } + + ConfigCatClient::closeAll(); +} diff --git a/test/test-evaluationlog.cpp b/test/test-evaluationlog.cpp new file mode 100644 index 0000000..b74beb8 --- /dev/null +++ b/test/test-evaluationlog.cpp @@ -0,0 +1,140 @@ +#include +#include "configcatlogger.h" +#include "configcat/fileoverridedatasource.h" +#include "configcat/configcatclient.h" +#include +#include "configcat/configcatuser.h" +#include "platform.h" +#include +#include "test.h" +#include "mock.h" + + +using namespace configcat; +using namespace std; +using json = nlohmann::json; + +class EvaluationLogTest : public ::testing::TestWithParam { +public: + static constexpr char kTestDataPath[] = "data/evaluation"; +}; + +INSTANTIATE_TEST_SUITE_P( + EvaluationLogTest, + EvaluationLogTest, + ::testing::Values( + "simple_value", + "1_targeting_rule", + "2_targeting_rules", + "options_based_on_user_id", + "options_based_on_custom_attr", + "options_after_targeting_rule", + "options_within_targeting_rule", + "and_rules", + "segment", + "prerequisite_flag", + "comparators", + "epoch_date_validation", + "number_validation", + "semver_validation", + "list_truncation" + )); + +TEST_P(EvaluationLogTest, TestEvaluationLog) { + string testSetName = GetParam(); + string directoryPath = RemoveFileName(__FILE__); + + string testSetDirectory = directoryPath + kTestDataPath + "/" + testSetName; + string testSetPath = testSetDirectory + ".json"; + ASSERT_TRUE(filesystem::exists(testSetPath)); + ifstream file(testSetPath); + json data = json::parse(file); + auto sdkKey = data.value("sdkKey", "local-only"); + auto baseUrl = data.value("baseUrl", ""); + auto jsonOverride = data.value("jsonOverride", ""); + + std::shared_ptr testLogger = make_shared(); + + ConfigCatOptions options; + options.pollingMode = PollingMode::manualPoll(); + options.logger = testLogger; + if (!baseUrl.empty()) { + options.baseUrl = baseUrl; + } + if (!jsonOverride.empty()) { + options.flagOverrides = make_shared(testSetDirectory + "/" + jsonOverride, LocalOnly); + } + + auto client = ConfigCatClient::get(sdkKey, &options); + client->forceRefresh(); + + for (const auto& test : data["tests"]) { + testLogger->clear(); + string key = test["key"]; + auto returnValue = test["returnValue"]; + auto defaultValue = test["defaultValue"]; + + std::shared_ptr user; + if (test.find("user") != test.end()) { + json userJson = test["user"]; + string id; + optional email; + optional country; + std::unordered_map custom = {}; + for (const auto& [k, v] : userJson.items()) { + if (k == "Identifier") { + id = v.get(); + } else if (k == "Email") { + email = v.get(); + } else if (k == "Country") { + country = v.get(); + } else if (v.is_string()) { + custom[k] = v.get(); + } else if (v.is_number()) { + custom[k] = v.get(); + } else { + throw std::runtime_error("Custom user attribute value type is invalid."); + } + } + + user = make_shared(id, email, country, custom); + } + + if (returnValue.is_boolean()) { + auto value = client->getValue(key, defaultValue.get(), user); + ASSERT_EQ(returnValue.get(), value); + } else if (returnValue.is_string()) { + auto value = client->getValue(key, defaultValue.get(), user); + ASSERT_EQ(returnValue.get(), value); + } else if (returnValue.is_number_integer()) { + auto value = client->getValue(key, defaultValue.get(), user); + ASSERT_EQ(returnValue.get(), value); + } else if (returnValue.is_number()) { + auto value = client->getValue(key, defaultValue.get(), user); + ASSERT_EQ(returnValue.get(), value); + } else { + throw std::runtime_error("Return value type is invalid."); + } + + auto expectedLogFile = test.value("expectedLog", ""); + auto expectedLogFilePath = testSetDirectory + "/" + expectedLogFile; + + // On Linux, the log file name is expected to be suffixed with "-linux" if it exists. + if (contains(getPlatformName(), "linux")) { + auto expectedLogFilePathLinux = expectedLogFilePath + ".linux"; + if (filesystem::exists(expectedLogFilePathLinux)) { + expectedLogFilePath = expectedLogFilePathLinux; + } + } + + ASSERT_TRUE(filesystem::exists(expectedLogFilePath)); + std::ifstream logFile(expectedLogFilePath); + std::stringstream buffer; + buffer << logFile.rdbuf(); + auto expectedLog = buffer.str(); + + ASSERT_EQ(expectedLog, testLogger->text); + } + + ConfigCatClient::closeAll(); +} diff --git a/test/test-hooks.cpp b/test/test-hooks.cpp index 49e3b7c..89ba2de 100644 --- a/test/test-hooks.cpp +++ b/test/test-hooks.cpp @@ -1,11 +1,11 @@ #include #include "mock.h" -#include "utils.h" #include "configservice.h" #include "configcat/configcatclient.h" #include "configcat/configcatoptions.h" -#include "configcat/configcatlogger.h" +#include "configcatlogger.h" #include +#include using namespace configcat; using namespace std; @@ -21,13 +21,11 @@ TEST_F(HooksTest, Init) { HookCallbacks hookCallbacks; auto hooks = make_shared( [&]() { hookCallbacks.onClientReady(); }, - [&](std::shared_ptr config) { hookCallbacks.onConfigChanged(config); }, - [&](const EvaluationDetails& details) { hookCallbacks.onFlagEvaluated(details); }, - [&](const string& error) { hookCallbacks.onError(error); } + [&](std::shared_ptr config) { hookCallbacks.onConfigChanged(config); }, + [&](const EvaluationDetailsBase& details) { hookCallbacks.onFlagEvaluated(details); }, + [&](const string& message, const std::exception_ptr& exception) { hookCallbacks.onError(message, exception); } ); - auto config = Config::fromJson(kTestJsonString); - ConfigCatOptions options; options.pollingMode = PollingMode::manualPoll(); options.configCache = make_shared(ConfigEntry( @@ -36,25 +34,31 @@ TEST_F(HooksTest, Init) { kTestJsonString).serialize() ); options.hooks = hooks; - auto client = ConfigCatClient::get("test", &options); + auto client = ConfigCatClient::get("test-67890123456789012/1234567890123456789012", &options); auto value = client->getValue("testStringKey", ""); + auto expectedConfig = Config::fromJson(kTestJsonString); + expectedConfig->preferences = {}; + expectedConfig->segments = {}; + Config actualConfig; + actualConfig.settings = make_shared(*hookCallbacks.changedConfig); + EXPECT_EQ("testValue", value); EXPECT_TRUE(hookCallbacks.isReady); EXPECT_EQ(1, hookCallbacks.isReadyCallCount); - EXPECT_EQ(*config->entries, *hookCallbacks.changedConfig); + EXPECT_EQ(expectedConfig->toJson(), actualConfig.toJson()); EXPECT_EQ(1, hookCallbacks.changedConfigCallCount); EXPECT_EQ("testStringKey", hookCallbacks.evaluationDetails.key); - EXPECT_EQ("testValue", get(hookCallbacks.evaluationDetails.value)); + EXPECT_EQ("testValue", get(*hookCallbacks.evaluationDetails.value)); EXPECT_EQ("id", hookCallbacks.evaluationDetails.variationId); EXPECT_TRUE(hookCallbacks.evaluationDetails.user == nullptr); EXPECT_FALSE(hookCallbacks.evaluationDetails.isDefaultValue); - EXPECT_TRUE(hookCallbacks.evaluationDetails.error.empty()); + EXPECT_FALSE(hookCallbacks.evaluationDetails.errorMessage.has_value()); EXPECT_EQ(1, hookCallbacks.evaluationDetailsCallCount); - EXPECT_TRUE(hookCallbacks.error.empty()); + EXPECT_TRUE(hookCallbacks.errorMessage.empty()); EXPECT_EQ(0, hookCallbacks.errorCallCount); ConfigCatClient::close(client); @@ -64,11 +68,9 @@ TEST_F(HooksTest, Subscribe) { HookCallbacks hookCallbacks; auto hooks = make_shared(); hooks->addOnClientReady([&]() { hookCallbacks.onClientReady(); }); - hooks->addOnConfigChanged([&](std::shared_ptr config) { hookCallbacks.onConfigChanged(config); }); - hooks->addOnFlagEvaluated([&](const EvaluationDetails& details) { hookCallbacks.onFlagEvaluated(details); }); - hooks->addOnError([&](const string& error) { hookCallbacks.onError(error); }); - - auto config = Config::fromJson(kTestJsonString); + hooks->addOnConfigChanged([&](std::shared_ptr config) { hookCallbacks.onConfigChanged(config); }); + hooks->addOnFlagEvaluated([&](const EvaluationDetailsBase& details) { hookCallbacks.onFlagEvaluated(details); }); + hooks->addOnError([&](const string& message, const std::exception_ptr& exception) { hookCallbacks.onError(message, exception); }); ConfigCatOptions options; options.pollingMode = PollingMode::manualPoll(); @@ -78,25 +80,31 @@ TEST_F(HooksTest, Subscribe) { kTestJsonString).serialize() ); options.hooks = hooks; - auto client = ConfigCatClient::get("test", &options); + auto client = ConfigCatClient::get("test-67890123456789012/1234567890123456789012", &options); auto value = client->getValue("testStringKey", ""); + auto expectedConfig = Config::fromJson(kTestJsonString); + expectedConfig->preferences = {}; + expectedConfig->segments = {}; + Config actualConfig; + actualConfig.settings = make_shared(*hookCallbacks.changedConfig); + EXPECT_EQ("testValue", value); EXPECT_TRUE(hookCallbacks.isReady); EXPECT_EQ(1, hookCallbacks.isReadyCallCount); - EXPECT_EQ(*config->entries, *hookCallbacks.changedConfig); + EXPECT_EQ(expectedConfig->toJson(), actualConfig.toJson()); EXPECT_EQ(1, hookCallbacks.changedConfigCallCount); EXPECT_EQ("testStringKey", hookCallbacks.evaluationDetails.key); - EXPECT_EQ("testValue", get(hookCallbacks.evaluationDetails.value)); + EXPECT_EQ("testValue", get(*hookCallbacks.evaluationDetails.value)); EXPECT_EQ("id", hookCallbacks.evaluationDetails.variationId); EXPECT_TRUE(hookCallbacks.evaluationDetails.user == nullptr); EXPECT_FALSE(hookCallbacks.evaluationDetails.isDefaultValue); - EXPECT_TRUE(hookCallbacks.evaluationDetails.error.empty()); + EXPECT_FALSE(hookCallbacks.evaluationDetails.errorMessage.has_value()); EXPECT_EQ(1, hookCallbacks.evaluationDetailsCallCount); - EXPECT_TRUE(hookCallbacks.error.empty()); + EXPECT_TRUE(hookCallbacks.errorMessage.empty()); EXPECT_EQ(0, hookCallbacks.errorCallCount); ConfigCatClient::close(client); @@ -110,30 +118,32 @@ TEST_F(HooksTest, Evaluation) { ConfigCatOptions options; options.pollingMode = PollingMode::manualPoll(); options.httpSessionAdapter = mockHttpSessionAdapter; - auto client = ConfigCatClient::get("test", &options); + auto client = ConfigCatClient::get("test-67890123456789012/1234567890123456789012", &options); - client->getHooks()->addOnFlagEvaluated([&](const EvaluationDetails& details) { hookCallbacks.onFlagEvaluated(details); }); + client->getHooks()->addOnFlagEvaluated([&](const EvaluationDetailsBase& details) { hookCallbacks.onFlagEvaluated(details); }); client->forceRefresh(); - ConfigCatUser user("test@test1.com"); - auto value = client->getValue("testStringKey", "", &user); + auto user = make_shared("test@test1.com"); + auto value = client->getValue("testStringKey", "", user); EXPECT_EQ("fake1", value); auto& details = hookCallbacks.evaluationDetails; - EXPECT_EQ("fake1", get(details.value)); + EXPECT_EQ("fake1", get(*details.value)); EXPECT_EQ("testStringKey", details.key); EXPECT_EQ("id1", details.variationId); EXPECT_FALSE(details.isDefaultValue); - EXPECT_TRUE(details.error.empty()); - EXPECT_TRUE(details.matchedEvaluationPercentageRule == std::nullopt); - - auto rule = details.matchedEvaluationRule; - EXPECT_EQ("fake1", get(rule->value)); - EXPECT_EQ(Comparator::CONTAINS, rule->comparator); - EXPECT_EQ("Identifier", rule->comparisonAttribute); - EXPECT_EQ("@test1.com", rule->comparisonValue); - EXPECT_TRUE(details.user == &user); + EXPECT_FALSE(details.errorMessage.has_value()); + EXPECT_TRUE(details.matchedPercentageOption == std::nullopt); + + auto& rule = details.matchedTargetingRule; + auto& condition = get(rule->conditions[0].condition); + auto& simpleValue = get(rule->then); + EXPECT_EQ("fake1", get(simpleValue.value)); + EXPECT_EQ(UserComparator::TextContainsAnyOf, condition.comparator); + EXPECT_EQ("Identifier", condition.comparisonAttribute); + EXPECT_EQ("@test1.com", get>(condition.comparisonValue)[0]); + EXPECT_TRUE(details.user == user); auto now = std::chrono::system_clock::now(); EXPECT_GT(details.fetchTime, now - std::chrono::seconds(1)); diff --git a/test/test-lazyloading.cpp b/test/test-lazyloading.cpp index 4c66a53..65d9ce4 100644 --- a/test/test-lazyloading.cpp +++ b/test/test-lazyloading.cpp @@ -1,9 +1,8 @@ #include #include "mock.h" -#include "utils.h" #include "configservice.h" #include "configcat/configcatoptions.h" -#include "configcat/configcatlogger.h" +#include "configcatlogger.h" #include "configcat/consolelogger.h" #include #include @@ -16,16 +15,16 @@ using namespace std::this_thread; class LazyLoadingTest : public ::testing::Test { public: static constexpr char kTestSdkKey[] = "TestSdkKey"; - static constexpr char kTestJsonFormat[] = R"({ "f": { "fakeKey": { "v": %s, "p": [], "r": [] } } })"; + static constexpr char kTestJsonFormat[] = R"({"f":{"fakeKey":{"t":%d,"v":%s}}})"; shared_ptr mockHttpSessionAdapter = make_shared(); shared_ptr logger = make_shared(make_shared(), make_shared()); }; TEST_F(LazyLoadingTest, Get) { - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; constexpr int secondResponseDelay = 2; mockHttpSessionAdapter->enqueueResponse(secondResponse, secondResponseDelay); @@ -50,9 +49,9 @@ TEST_F(LazyLoadingTest, Get) { } TEST_F(LazyLoadingTest, GetFailedRequest) { - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {500, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {500, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(secondResponse); ConfigCatOptions options; @@ -78,9 +77,9 @@ TEST_F(LazyLoadingTest, GetFailedRequest) { TEST_F(LazyLoadingTest, Cache) { auto mockCache = make_shared(); - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(secondResponse); ConfigCatOptions options; @@ -92,7 +91,7 @@ TEST_F(LazyLoadingTest, Cache) { EXPECT_EQ("test", std::get(settings["fakeKey"].value)); EXPECT_EQ(1, mockCache->store.size()); - EXPECT_TRUE(contains(mockCache->store.begin()->second, R"("test")")); + EXPECT_TRUE(contains(mockCache->store.begin()->second, R"({"s":"test"})")); // Wait for cache invalidation sleep_for(seconds(3)); @@ -101,19 +100,19 @@ TEST_F(LazyLoadingTest, Cache) { EXPECT_EQ("test2", std::get(settings["fakeKey"].value)); EXPECT_EQ(1, mockCache->store.size()); - EXPECT_TRUE(contains(mockCache->store.begin()->second, R"("test2")")); + EXPECT_TRUE(contains(mockCache->store.begin()->second, R"({"s":"test2"})")); } TEST_F(LazyLoadingTest, ReturnCachedConfigWhenCacheIsNotExpired) { - auto jsonString = string_format(kTestJsonFormat, R"("test")"); + auto jsonString = string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})"); auto mockCache = make_shared(ConfigEntry( Config::fromJson(jsonString), "test-etag", jsonString, - getUtcNowSecondsSinceEpoch()).serialize() + get_utcnowseconds_since_epoch()).serialize() ); - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); ConfigCatOptions options; @@ -136,15 +135,15 @@ TEST_F(LazyLoadingTest, ReturnCachedConfigWhenCacheIsNotExpired) { TEST_F(LazyLoadingTest, FetchConfigWhenCacheIsExpired) { auto cacheTimeToLiveSeconds = 1; - auto jsonString = string_format(kTestJsonFormat, R"("test")"); + auto jsonString = string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})"); auto mockCache = make_shared(ConfigEntry( Config::fromJson(jsonString), "test-etag", jsonString, - getUtcNowSecondsSinceEpoch() - cacheTimeToLiveSeconds).serialize() + get_utcnowseconds_since_epoch() - cacheTimeToLiveSeconds).serialize() ); - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); ConfigCatOptions options; @@ -157,8 +156,44 @@ TEST_F(LazyLoadingTest, FetchConfigWhenCacheIsExpired) { EXPECT_EQ(1, mockHttpSessionAdapter->requests.size()); } +TEST_F(LazyLoadingTest, CacheTTLRespectsExternalCache) { + auto cacheTimeToLiveSeconds = 1; + auto jsonStringLocal = string_format(kTestJsonFormat, SettingType::String, R"({"s":"test-local"})"); + auto mockCache = make_shared(ConfigEntry( + Config::fromJson(jsonStringLocal), + "etag", + jsonStringLocal, + get_utcnowseconds_since_epoch()).serialize() + ); + + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test-remote"})")}; + mockHttpSessionAdapter->enqueueResponse(firstResponse); + + ConfigCatOptions options; + options.pollingMode = PollingMode::lazyLoad(cacheTimeToLiveSeconds); + options.httpSessionAdapter = mockHttpSessionAdapter; + auto service = ConfigService(kTestSdkKey, logger, make_shared(), mockCache, options); + + auto settings = *service.getSettings().settings; + EXPECT_EQ("test-local", std::get(settings["fakeKey"].value)); + EXPECT_EQ(0, mockHttpSessionAdapter->requests.size()); + + sleep_for(seconds(1)); + + jsonStringLocal = string_format(kTestJsonFormat, SettingType::String, R"({"s":"test-local2"})"); + mockCache->value = ConfigEntry( + Config::fromJson(jsonStringLocal), + "etag2", + jsonStringLocal, + get_utcnowseconds_since_epoch()).serialize(); + + settings = *service.getSettings().settings; + EXPECT_EQ("test-local2", std::get(settings["fakeKey"].value)); + EXPECT_EQ(0, mockHttpSessionAdapter->requests.size()); +} + TEST_F(LazyLoadingTest, OnlineOffline) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(response); ConfigCatOptions options; @@ -189,7 +224,7 @@ TEST_F(LazyLoadingTest, OnlineOffline) { } TEST_F(LazyLoadingTest, InitOffline) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(response); ConfigCatOptions options; diff --git a/test/test-log.cpp b/test/test-log.cpp index 8ab811f..67510f9 100644 --- a/test/test-log.cpp +++ b/test/test-log.cpp @@ -1,51 +1,17 @@ #include -#include "configcat/configcatlogger.h" -#include +#include "mock.h" using namespace configcat; using namespace std; -using json = nlohmann::json; class LogTest : public ::testing::Test { public: - class TestLogger : public ILogger { - public: - TestLogger(): ILogger(LOG_LEVEL_INFO) {} - void log(LogLevel level, const std::string& message) override { - text += message + "\n"; - } - std::string text; - }; - std::shared_ptr testLogger = make_shared(); shared_ptr logger = make_shared(testLogger, make_shared()); }; -TEST_F(LogTest, LogUser) { - auto user = ConfigCatUser( - "id", - "email", - "country", - {{"custom", "test"}} - ); - LOG_INFO(0) << user; - - json userJson = json::parse(testLogger->text.substr(4)); - - EXPECT_EQ("id", userJson["Identifier"]); - EXPECT_EQ("email", userJson["Email"]); - EXPECT_EQ("country", userJson["Country"]); - EXPECT_EQ("test", userJson["custom"]); -} - -TEST_F(LogTest, LogIntVector) { - std::vector v = { 1, 2, 3 }; - LOG_INFO(5000) << v; - EXPECT_EQ("[5000] [1, 2, 3]\n", testLogger->text); -} - -TEST_F(LogTest, LogStringVector) { +TEST_F(LogTest, LogStringVector1) { std::vector v = { "a", "b", "c" }; LOG_INFO(5000) << v; - EXPECT_EQ("[5000] [a, b, c]\n", testLogger->text); + EXPECT_EQ("INFO [5000] ['a', 'b', 'c']\n", testLogger->text); } diff --git a/test/test-manualpolling.cpp b/test/test-manualpolling.cpp index 148f860..e85abe1 100644 --- a/test/test-manualpolling.cpp +++ b/test/test-manualpolling.cpp @@ -1,11 +1,9 @@ #include #include "mock.h" -#include "utils.h" #include "configservice.h" #include "configcat/configcatoptions.h" -#include "configcat/configcatlogger.h" +#include "configcatlogger.h" #include "configcat/consolelogger.h" -#include using namespace configcat; using namespace std; @@ -15,16 +13,16 @@ using namespace std::this_thread; class ManualPollingTest : public ::testing::Test { public: static constexpr char kTestSdkKey[] = "TestSdkKey"; - static constexpr char kTestJsonFormat[] = R"({ "f": { "fakeKey": { "v": %s, "p": [], "r": [] } } })"; + static constexpr char kTestJsonFormat[] = R"({"f":{"fakeKey":{"t":%d,"v":%s}}})"; shared_ptr mockHttpSessionAdapter = make_shared(); shared_ptr logger = make_shared(make_shared(), make_shared()); }; TEST_F(ManualPollingTest, Get) { - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; constexpr int secondResponseDelay = 2; mockHttpSessionAdapter->enqueueResponse(secondResponse, secondResponseDelay); @@ -48,9 +46,9 @@ TEST_F(ManualPollingTest, Get) { } TEST_F(ManualPollingTest, GetFailedRefresh) { - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {500, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {500, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(secondResponse); ConfigCatOptions options; @@ -72,9 +70,9 @@ TEST_F(ManualPollingTest, GetFailedRefresh) { TEST_F(ManualPollingTest, Cache) { auto mockCache = make_shared(); - configcat::Response firstResponse = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response firstResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(firstResponse); - configcat::Response secondResponse = {200, string_format(kTestJsonFormat, R"("test2")")}; + configcat::Response secondResponse = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test2"})")}; mockHttpSessionAdapter->enqueueResponse(secondResponse); ConfigCatOptions options; @@ -88,7 +86,7 @@ TEST_F(ManualPollingTest, Cache) { EXPECT_EQ("test", std::get(settings["fakeKey"].value)); EXPECT_EQ(1, mockCache->store.size()); - EXPECT_TRUE(contains(mockCache->store.begin()->second, R"("test")")); + EXPECT_TRUE(contains(mockCache->store.begin()->second, R"({"s":"test"})")); service.refresh(); @@ -96,11 +94,11 @@ TEST_F(ManualPollingTest, Cache) { EXPECT_EQ("test2", std::get(settings["fakeKey"].value)); EXPECT_EQ(1, mockCache->store.size()); - EXPECT_TRUE(contains(mockCache->store.begin()->second, R"("test2")")); + EXPECT_TRUE(contains(mockCache->store.begin()->second, R"({"s":"test2"})")); } TEST_F(ManualPollingTest, EmptyCacheDoesNotInitiateHTTP) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(response); ConfigCatOptions options; @@ -115,7 +113,7 @@ TEST_F(ManualPollingTest, EmptyCacheDoesNotInitiateHTTP) { } TEST_F(ManualPollingTest, OnlineOffline) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(response); ConfigCatOptions options; @@ -124,25 +122,27 @@ TEST_F(ManualPollingTest, OnlineOffline) { auto service = ConfigService(kTestSdkKey, logger, make_shared(), make_shared(), options); EXPECT_FALSE(service.isOffline()); - EXPECT_TRUE(service.refresh().success); + EXPECT_TRUE(service.refresh().success()); auto settings = *service.getSettings().settings; EXPECT_EQ("test", std::get(settings["fakeKey"].value)); EXPECT_EQ(1, mockHttpSessionAdapter->requests.size()); service.setOffline(); EXPECT_TRUE(service.isOffline()); - EXPECT_FALSE(service.refresh().success); + EXPECT_FALSE(service.refresh().success()); EXPECT_EQ(1, mockHttpSessionAdapter->requests.size()); service.setOnline(); + mockHttpSessionAdapter->enqueueResponse(response); + EXPECT_FALSE(service.isOffline()); - EXPECT_TRUE(service.refresh().success); + EXPECT_TRUE(service.refresh().success()); EXPECT_EQ(2, mockHttpSessionAdapter->requests.size()); } TEST_F(ManualPollingTest, InitOffline) { - configcat::Response response = {200, string_format(kTestJsonFormat, R"("test")")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::String, R"({"s":"test"})")}; mockHttpSessionAdapter->enqueueResponse(response); ConfigCatOptions options; @@ -152,13 +152,13 @@ TEST_F(ManualPollingTest, InitOffline) { auto service = ConfigService(kTestSdkKey, logger, make_shared(), make_shared(), options); EXPECT_TRUE(service.isOffline()); - EXPECT_FALSE(service.refresh().success); + EXPECT_FALSE(service.refresh().success()); EXPECT_EQ(0, mockHttpSessionAdapter->requests.size()); service.setOnline(); EXPECT_FALSE(service.isOffline()); - EXPECT_TRUE(service.refresh().success); + EXPECT_TRUE(service.refresh().success()); auto settings = *service.getSettings().settings; EXPECT_EQ("test", std::get(settings["fakeKey"].value)); EXPECT_EQ(1, mockHttpSessionAdapter->requests.size()); diff --git a/test/test-override.cpp b/test/test-override.cpp index 0eacf1b..a2e3563 100644 --- a/test/test-override.cpp +++ b/test/test-override.cpp @@ -2,10 +2,9 @@ #include "configcat/mapoverridedatasource.h" #include "configcat/fileoverridedatasource.h" #include "configcat/configcatclient.h" -#include "configcat/configcatlogger.h" +#include "configcatlogger.h" #include "configcat/consolelogger.h" #include "mock.h" -#include "utils.h" #include "test.h" #include #include @@ -43,8 +42,8 @@ class OverrideTest : public ::testing::Test { string directoryPath = RemoveFileName(__FILE__); vector tempFiles; - static constexpr char kTestSdkKey[] = "TestSdkKey"; - static constexpr char kTestJsonFormat[] = R"({ "f": { "fakeKey": { "v": %s, "p": [], "r": [] } } })"; + static constexpr char kTestSdkKey[] = "TestSdkKey-23456789012/1234567890123456789012"; + static constexpr char kTestJsonFormat[] = R"({"f":{"fakeKey":{"t":%d,"v":%s}}})"; ConfigCatClient* client = nullptr; shared_ptr mockHttpSessionAdapter = make_shared(); shared_ptr logger = make_shared(make_shared(), make_shared()); @@ -74,7 +73,7 @@ TEST_F(OverrideTest, Map) { } TEST_F(OverrideTest, LocalOverRemote) { - configcat::Response response = {200, string_format(kTestJsonFormat, "false")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::Boolean, R"({"b":false})")}; mockHttpSessionAdapter->enqueueResponse(response); const std::unordered_map& map = { @@ -96,7 +95,7 @@ TEST_F(OverrideTest, LocalOverRemote) { } TEST_F(OverrideTest, RemoteOverLocal) { - configcat::Response response = {200, string_format(kTestJsonFormat, "false")}; + configcat::Response response = {200, string_format(kTestJsonFormat, SettingType::Boolean, R"({"b":false})")}; mockHttpSessionAdapter->enqueueResponse(response); const std::unordered_map& map = { diff --git a/test/test-rolloutintegration.cpp b/test/test-rolloutintegration.cpp index 3152a0c..71ab987 100644 --- a/test/test-rolloutintegration.cpp +++ b/test/test-rolloutintegration.cpp @@ -4,9 +4,8 @@ #include #include "configcat/configcatuser.h" #include "configcat/configcatclient.h" -#include "configcat/configcatlogger.h" +#include "configcatlogger.h" #include "configcat/consolelogger.h" -#include "utils.h" #include "test.h" #include "configcat/log.h" @@ -59,10 +58,10 @@ class RolloutIntegrationTest : public ::testing::Test { list errors; for (int i = 1; i < matrixData.size(); ++i) { auto& testObjects = matrixData[i]; - unique_ptr user; + shared_ptr user; if (testObjects[0] != "##null##") { - string email; - string country; + std::optional email; + std::optional country; string identifier = testObjects[0]; if (!testObjects[1].empty() && testObjects[1] != "##null##") { email = testObjects[1]; @@ -71,30 +70,44 @@ class RolloutIntegrationTest : public ::testing::Test { country = testObjects[2]; } - unordered_map custom; + unordered_map custom; if (!testObjects[3].empty() && testObjects[3] != "##null##") { custom[customKey] = testObjects[3]; } - user = make_unique(identifier, email, country, custom); + user = make_shared(identifier, email, country, custom); } int j = 0; for (auto& settingKey : settingKeys) { - string expected = str_tolower(testObjects[j + 4]); + string expected = testObjects[j + 4]; if (isValueKind) { - auto value = client->getValue(settingKey, user.get()); - if (!value || str_tolower(valueToString(*value)) != expected) { + auto value = client->getValue(settingKey, user); + + auto success = value.has_value(); + if (success) { + if (holds_alternative(*value)) { + success = get(*value) ? expected == "True" : expected == "False"; + } else if (holds_alternative(*value)) { + success = get(*value) == expected; + } else if (holds_alternative(*value)) { + success = get(*value) == stoi(expected); + } else { + success = get(*value) == stod(expected); + } + } + + if (!success) { errors.push_back(string_format("Index: [%d:%d] Identifier: %s, Key: %s. UV: %s Expected: %s, Result: %s", i, j, testObjects[0].c_str(), settingKey.c_str(), testObjects[3].c_str(), expected.c_str(), - value ? valueToString(*value).c_str() : "##null##")); + value ? value->toString().c_str() : "##null##")); } } else { - auto details = client->getValueDetails(settingKey, "", user.get()); + auto details = client->getValueDetails(settingKey, user); auto variationId = details.variationId; if (variationId != expected) { errors.push_back( @@ -103,7 +116,7 @@ class RolloutIntegrationTest : public ::testing::Test { testObjects[0].c_str(), settingKey.c_str(), expected.c_str(), - variationId.c_str())); + variationId.value_or("").c_str())); } } ++j; @@ -120,26 +133,99 @@ class RolloutIntegrationTest : public ::testing::Test { } }; -TEST_F(RolloutIntegrationTest, RolloutMatrixText) { +// *** Config V1 *** + +TEST_F(RolloutIntegrationTest, RolloutMatrixText_v1) { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d testRolloutMatrix(directoryPath + "data/testmatrix.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", true); } -TEST_F(RolloutIntegrationTest, RolloutMatrixSemantic) { +TEST_F(RolloutIntegrationTest, RolloutMatrixSemantic_v1) { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d testRolloutMatrix(directoryPath + "data/testmatrix_semantic.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", true); } -TEST_F(RolloutIntegrationTest, RolloutMatrixSemantic2) { +TEST_F(RolloutIntegrationTest, RolloutMatrixNumber_v1) { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d + testRolloutMatrix(directoryPath + "data/testmatrix_number.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", true); +} + +TEST_F(RolloutIntegrationTest, RolloutMatrixSemantic2_v1) { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d77fa1-a796-85f9-df0c-57c448eb9934/244cf8b0-f604-11e8-b543-f23c917f9d8d testRolloutMatrix(directoryPath + "data/testmatrix_semantic_2.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/q6jMCFIp-EmuAfnmZhPY7w", true); } +TEST_F(RolloutIntegrationTest, RolloutMatrixSensitive_v1) { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d7b724-9285-f4a7-9fcd-00f64f1e83d5/244cf8b0-f604-11e8-b543-f23c917f9d8d + testRolloutMatrix(directoryPath + "data/testmatrix_sensitive.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA", true); +} + +TEST_F(RolloutIntegrationTest, RolloutMatrixSegmentsOld_v1) { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d9f207-6883-43e5-868c-cbf677af3fe6/244cf8b0-f604-11e8-b543-f23c917f9d8d + testRolloutMatrix(directoryPath + "data/testmatrix_segments_old.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/LcYz135LE0qbcacz2mgXnA", true); +} + +TEST_F(RolloutIntegrationTest, RolloutMatrixVariationId_v1) { + // https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d774b9-3d05-0027-d5f4-3e76c3dba752/244cf8b0-f604-11e8-b543-f23c917f9d8d + testRolloutMatrix(directoryPath + "data/testmatrix_variationId.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA", false); +} + +// *** Config V2 *** + +TEST_F(RolloutIntegrationTest, RolloutMatrixText) { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-1927-4d6b-8fb9-b1472564e2d3/244cf8b0-f604-11e8-b543-f23c917f9d8d + testRolloutMatrix(directoryPath + "data/testmatrix.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/AG6C1ngVb0CvM07un6JisQ", true); +} + +TEST_F(RolloutIntegrationTest, RolloutMatrixSemantic) { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-1927-4d6b-8fb9-b1472564e2d3/244cf8b0-f604-11e8-b543-f23c917f9d8d + testRolloutMatrix(directoryPath + "data/testmatrix_semantic.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/iV8vH2MBakKxkFZylxHmTg", true); +} + TEST_F(RolloutIntegrationTest, RolloutMatrixNumber) { - testRolloutMatrix(directoryPath + "data/testmatrix_number.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", true); + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-0fa3-48d0-8de8-9de55b67fb8b/244cf8b0-f604-11e8-b543-f23c917f9d8d + testRolloutMatrix(directoryPath + "data/testmatrix_number.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/FCWN-k1dV0iBf8QZrDgjdw", true); +} + +TEST_F(RolloutIntegrationTest, RolloutMatrixSemantic2) { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-2b2b-451e-8359-abdef494c2a2/244cf8b0-f604-11e8-b543-f23c917f9d8d + testRolloutMatrix(directoryPath + "data/testmatrix_semantic_2.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/U8nt3zEhDEO5S2ulubCopA", true); } TEST_F(RolloutIntegrationTest, RolloutMatrixSensitive) { - testRolloutMatrix(directoryPath + "data/testmatrix_sensitive.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/qX3TP2dTj06ZpCCT1h_SPA", true); + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-2d62-4e1b-884b-6aa237b34764/244cf8b0-f604-11e8-b543-f23c917f9d8d + testRolloutMatrix(directoryPath + "data/testmatrix_sensitive.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/-0YmVOUNgEGKkgRF-rU65g", true); +} + +TEST_F(RolloutIntegrationTest, RolloutMatrixSegmentsOld) { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbd6ca-a85f-4ed0-888a-2da18def92b5/244cf8b0-f604-11e8-b543-f23c917f9d8d + testRolloutMatrix(directoryPath + "data/testmatrix_segments_old.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", true); } TEST_F(RolloutIntegrationTest, RolloutMatrixVariationId) { - testRolloutMatrix(directoryPath + "data/testmatrix_variationId.csv", "PKDVCLf-Hq-h-kCzMp-L7Q/nQ5qkhRAUEa6beEyyrVLBA", false); + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbc4dc-30c6-4969-8e4c-03f6a8764199/244cf8b0-f604-11e8-b543-f23c917f9d8d + testRolloutMatrix(directoryPath + "data/testmatrix_variationId.csv", "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/spQnkRTIPEWVivZkWM84lQ", false); +} + +TEST_F(RolloutIntegrationTest, RolloutMatrixAndOr) { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb + testRolloutMatrix(directoryPath + "data/testmatrix_and_or.csv", "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", true); +} + +TEST_F(RolloutIntegrationTest, RolloutMatrixComparatorsV6) { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb + testRolloutMatrix(directoryPath + "data/testmatrix_comparators_v6.csv", "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", true); +} + +TEST_F(RolloutIntegrationTest, RolloutMatrixPrerequisiteFlag) { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9b74-45cb-86d0-4d61c25af1aa/08dbc325-9ebd-4587-8171-88f76a3004cb + testRolloutMatrix(directoryPath + "data/testmatrix_prerequisite_flag.csv", "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/JoGwdqJZQ0K2xDy7LnbyOg", true); +} +TEST_F(RolloutIntegrationTest, RolloutMatrixSegments) { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9b74-45cb-86d0-4d61c25af1aa/08dbc325-9ebd-4587-8171-88f76a3004cb + testRolloutMatrix(directoryPath + "data/testmatrix_segments.csv", "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/h99HYXWWNE2bH8eWyLAVMA", true); +} +TEST_F(RolloutIntegrationTest, RolloutMatrixUnicode) { + // https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9b74-45cb-86d0-4d61c25af1aa/08dbc325-9ebd-4587-8171-88f76a3004cb + testRolloutMatrix(directoryPath + "data/testmatrix_unicode.csv", "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/Da6w8dBbmUeMUBhh0iEeQQ", true); } diff --git a/test/test-specialcharacter.cpp b/test/test-specialcharacter.cpp new file mode 100644 index 0000000..be32dd3 --- /dev/null +++ b/test/test-specialcharacter.cpp @@ -0,0 +1,31 @@ +#include +#include "configcat/configcatuser.h" +#include "configcat/configcatclient.h" + + +using namespace std; +using namespace configcat; + +using MatrixData = vector>; + +class SpecialCharacterTest : public ::testing::Test { +public: + shared_ptr client = nullptr; + void SetUp() override { + client = ConfigCatClient::get("configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/u28_1qNyZ0Wz-ldYHIU7-g"); + } + + void TearDown() override { + ConfigCatClient::closeAll(); + } +}; + +TEST_F(SpecialCharacterTest, SpecialCharactersWorksCleartext) { + string actual = client->getValue("specialCharacters", "NOT_CAT", make_shared("äöüÄÖÜçéèñışğ⢙✓😀")); + EXPECT_EQ(actual, "äöüÄÖÜçéèñışğ⢙✓😀"); +} + +TEST_F(SpecialCharacterTest, SpecialCharactersWorksHashed) { + string actual = client->getValue("specialCharactersHashed", "NOT_CAT", make_shared("äöüÄÖÜçéèñışğ⢙✓😀")); + EXPECT_EQ(actual, "äöüÄÖÜçéèñışğ⢙✓😀"); +} diff --git a/test/test-utils.cpp b/test/test-utils.cpp new file mode 100644 index 0000000..a030656 --- /dev/null +++ b/test/test-utils.cpp @@ -0,0 +1,214 @@ +#include +#include +#include +#include "configcat/timeutils.h" +#include "utils.h" + +using namespace configcat; +using namespace std; + +TEST(UtilsTest, string_format_test_1) { + auto s = string_format(""); + + ASSERT_EQ("", s); + ASSERT_EQ(0, (int)s[s.length()]); +} + +TEST(UtilsTest, string_format_test_2) { + auto s = string_format("", "x"); + + ASSERT_EQ("", s); + ASSERT_EQ(0, (int)s[s.length()]); +} + +TEST(UtilsTest, string_format_test_3) { + string format(STRING_FORMAT_STACKBUF_MAXSIZE - 1, 'a'); + auto s = string_format(format); + + ASSERT_EQ(format, s); + ASSERT_EQ(0, (int)s[s.length()]); +} + +TEST(UtilsTest, string_format_test_4) { + string format(STRING_FORMAT_STACKBUF_MAXSIZE, 'a'); + auto s = string_format(format); + + ASSERT_EQ(format, s); + ASSERT_EQ(0, (int)s[s.length()]); +} + +TEST(UtilsTest, string_format_test_5) { + auto s = string_format("a%sc", "b"); + + ASSERT_EQ("abc", s); + ASSERT_EQ(0, (int)s[s.length()]); +} + +TEST(UtilsTest, string_format_test_6) { + auto s1 = string(STRING_FORMAT_STACKBUF_MAXSIZE, 'a'); + auto s2 = string(STRING_FORMAT_STACKBUF_MAXSIZE, 'b'); + auto s = string_format("%s-%s", s1.c_str(), s2.c_str()); + + ASSERT_EQ(s1 + '-' + s2, s); + ASSERT_EQ(0, (int)s[s.length()]); +} + +TEST(UtilsTest, trim_test_1) { + string s(" \t\r abc \n"); + trim(s); + + ASSERT_EQ("abc", s); +} + +TEST(UtilsTest, datetime_to_isostring_test) { + auto s = datetime_to_isostring(*datetime_from_unixtimeseconds(0)); + + ASSERT_EQ(string("1970-01-01T00:00:00.000Z"), s); +} + +class datetime_to_isostring_testsuite : public ::testing::TestWithParam> { +}; + +TEST_P(datetime_to_isostring_testsuite, numberToStringTest) { + auto [input, expectedOutput] = GetParam(); + auto actualOutput = number_to_string(input); + + ASSERT_EQ(expectedOutput, actualOutput); +} + +INSTANTIATE_TEST_SUITE_P( + UtilsTest, + datetime_to_isostring_testsuite, + ::testing::Values( + std::make_tuple(NAN, "NaN"), + std::make_tuple(INFINITY, "Infinity"), + std::make_tuple(-INFINITY, "-Infinity"), + std::make_tuple(0, "0"), + std::make_tuple(1, "1"), + std::make_tuple(-1, "-1"), + std::make_tuple(0.1, "0.1"), + std::make_tuple(-0.1, "-0.1"), + std::make_tuple(1e-6, "0.000001"), + std::make_tuple(-1e-6, "-0.000001"), + std::make_tuple(0.99e-6, "9.9e-7"), + std::make_tuple(-0.99e-6, "-9.9e-7"), + std::make_tuple(0.99e21, "990000000000000000000"), + std::make_tuple(-0.99e21, "-990000000000000000000"), + std::make_tuple(1e21, "1e+21"), + std::make_tuple(-1e21, "-1e+21"), + std::make_tuple(1.000000000000000056e-01, "0.1"), + std::make_tuple(1.199999999999999956e+00, "1.2"), + std::make_tuple(1.229999999999999982e+00, "1.23"), + std::make_tuple(1.233999999999999986e+00, "1.234"), + std::make_tuple(1.234499999999999931e+00, "1.2345"), + std::make_tuple(1.002000000000000028e+02, "100.2"), + std::make_tuple(1.030000000000000000e+05, "103000"), + std::make_tuple(1.003001000000000005e+02, "100.3001"), + std::make_tuple(-1.000000000000000056e-01, "-0.1"), + std::make_tuple(-1.199999999999999956e+00, "-1.2"), + std::make_tuple(-1.229999999999999982e+00, "-1.23"), + std::make_tuple(-1.233999999999999986e+00, "-1.234"), + std::make_tuple(-1.234499999999999931e+00, "-1.2345"), + std::make_tuple(-1.002000000000000028e+02, "-100.2"), + std::make_tuple(-1.030000000000000000e+05, "-103000"), + std::make_tuple(-1.003001000000000005e+02, "-100.3001") + )); + +class number_from_string_testsuite : public ::testing::TestWithParam>> { +}; + +TEST_P(number_from_string_testsuite, number_from_string_test) { + auto [input, expectedOutput] = GetParam(); + string inputCopy(input); + auto actualOutput = number_from_string(input); + + ASSERT_EQ(input, inputCopy); + ASSERT_EQ(expectedOutput.has_value(), actualOutput.has_value()); + if (expectedOutput.has_value()) { + if (!isnan(*expectedOutput)) { + ASSERT_EQ(*expectedOutput, *actualOutput); + } + else { + ASSERT_TRUE(isnan(*actualOutput)); + } + } +} + +INSTANTIATE_TEST_SUITE_P( + UtilsTest, + number_from_string_testsuite, + ::testing::Values( + std::make_tuple("", nullopt), + std::make_tuple(" ", nullopt), + std::make_tuple("NaN", NAN), + std::make_tuple("Infinity", INFINITY), + std::make_tuple("+Infinity", INFINITY), + std::make_tuple("-Infinity", -INFINITY), + std::make_tuple("1", 1), + std::make_tuple("1 ", 1), + std::make_tuple(" 1", 1), + std::make_tuple(" 1 ", 1), + std::make_tuple("0x1", nullopt), + std::make_tuple(" 0x1", nullopt), + std::make_tuple("+0x1", nullopt), + std::make_tuple("-0x1", nullopt), + std::make_tuple("1f", nullopt), + std::make_tuple("1e", nullopt), + std::make_tuple("0+", nullopt), + std::make_tuple("0-", nullopt), + std::make_tuple("2023.11.13", nullopt), + std::make_tuple("0", 0), + std::make_tuple("-0", 0), + std::make_tuple("+0", 0), + std::make_tuple("1234567890", 1234567890), + std::make_tuple("1234567890.0", 1234567890), + std::make_tuple("1234567890e0", 1234567890), + std::make_tuple(".1234567890", 0.1234567890), + std::make_tuple("+.1234567890", 0.1234567890), + std::make_tuple("-.1234567890", -0.1234567890), + std::make_tuple("+0.123e-3", 0.000123), + std::make_tuple("-0.123e+3", -123) + )); + +class integer_from_string_testsuite : public ::testing::TestWithParam>> { +}; + +TEST_P(integer_from_string_testsuite, integer_from_string_test) { + auto [input, expectedOutput] = GetParam(); + string inputCopy(input); + auto actualOutput = integer_from_string(input); + + ASSERT_EQ(input, inputCopy); + ASSERT_EQ(expectedOutput.has_value(), actualOutput.has_value()); + if (expectedOutput.has_value()) { + ASSERT_EQ(*expectedOutput, *actualOutput); + } +} + +INSTANTIATE_TEST_SUITE_P( + UtilsTest, + integer_from_string_testsuite, + ::testing::Values( + std::make_tuple("", nullopt), + std::make_tuple(" ", nullopt), + std::make_tuple("NaN", nullopt), + std::make_tuple("Infinity", nullopt), + std::make_tuple("1", 1), + std::make_tuple("1 ", 1), + std::make_tuple(" 1", 1), + std::make_tuple(" 1 ", 1), + std::make_tuple("0x1", nullopt), + std::make_tuple(" 0x1", nullopt), + std::make_tuple("+0x1", nullopt), + std::make_tuple("-0x1", nullopt), + std::make_tuple("1f", nullopt), + std::make_tuple("1e", nullopt), + std::make_tuple("0+", nullopt), + std::make_tuple("0-", nullopt), + std::make_tuple("2023.11.13", nullopt), + std::make_tuple("0", 0), + std::make_tuple("-0", 0), + std::make_tuple("+0", 0), + std::make_tuple("1234567890", 1234567890), + std::make_tuple("0777", 777) + )); diff --git a/test/test-variationid.cpp b/test/test-variationid.cpp index 9b121f8..8c1460f 100644 --- a/test/test-variationid.cpp +++ b/test/test-variationid.cpp @@ -2,7 +2,6 @@ #include "mock.h" #include "configcat/configcatclient.h" #include "configcat/config.h" -#include "utils.h" using namespace configcat; @@ -10,51 +9,83 @@ using namespace std; class VariationIdTest : public ::testing::Test { public: - static constexpr char kTestSdkKey[] = "TestSdkKey"; - static constexpr char kTestJson[] = R"( - {"f":{ - "key1":{ - "v":true, - "i":"fakeId1", - "p":[ - { - "v":true, - "p":50, - "i":"percentageId1" - }, - { - "v":false, - "p":50, - "i":"percentageId2" - } - ], - "r":[ - { - "a":"Email", - "t":2, - "c":"@configcat.com", - "v":true, - "i":"rolloutId1" - }, - { - "a":"Email", - "t":2, - "c":"@test.com", - "v":false, - "i":"rolloutId2" - } - ] - }, - "key2":{ - "v":false, - "i":"fakeId2", - "p":[], - "r":[] - } - }} - )"; - - ConfigCatClient* client = nullptr; + static constexpr char kTestSdkKey[] = "TestSdkKey-23456789012/1234567890123456789012"; + static constexpr char kTestJson[] = R"({ + "f": { + "key1": { + "t": 0, + "r": [ + { + "c": [ + { + "u": { + "a": "Email", + "c": 2, + "l": [ + "@configcat.com" + ] + } + } + ], + "s": { + "v": { + "b": true + }, + "i": "rolloutId1" + } + }, + { + "c": [ + { + "u": { + "a": "Email", + "c": 2, + "l": [ + "@test.com" + ] + } + } + ], + "s": { + "v": { + "b": false + }, + "i": "rolloutId2" + } + } + ], + "p": [ + { + "p": 50, + "v": { + "b": true + }, + "i": "percentageId1" + }, + { + "p": 50, + "v": { + "b": false + }, + "i": "percentageId2" + } + ], + "v": { + "b": true + }, + "i": "fakeId1" + }, + "key2": { + "t": 0, + "v": { + "b": false + }, + "i": "fakeId2" + } + } + })"; + + shared_ptr client = nullptr; shared_ptr mockHttpSessionAdapter = make_shared(); void SetUp() override { @@ -74,9 +105,10 @@ TEST_F(VariationIdTest, GetVariationId) { configcat::Response response = {200, kTestJson}; mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); - auto details = client->getValueDetails("key1", ""); + auto details = client->getValueDetails("key1", false); - EXPECT_EQ("fakeId1", details.variationId); + EXPECT_TRUE(details.variationId.has_value()); + EXPECT_EQ("fakeId1", *details.variationId); } TEST_F(VariationIdTest, GetVariationIdNotFound) { @@ -84,7 +116,8 @@ TEST_F(VariationIdTest, GetVariationIdNotFound) { mockHttpSessionAdapter->enqueueResponse(response); client->forceRefresh(); auto details = client->getValueDetails("nonexisting", "default"); - EXPECT_EQ("", details.variationId); + + EXPECT_FALSE(details.variationId.has_value()); } TEST_F(VariationIdTest, GetVarationIdInvalidJson) { @@ -93,7 +126,7 @@ TEST_F(VariationIdTest, GetVarationIdInvalidJson) { client->forceRefresh(); auto details = client->getValueDetails("key1", ""); - EXPECT_EQ("", details.variationId); + EXPECT_FALSE(details.variationId.has_value()); } TEST_F(VariationIdTest, GetAllVariationIds) { @@ -103,10 +136,10 @@ TEST_F(VariationIdTest, GetAllVariationIds) { auto allDetails = client->getAllValueDetails(); EXPECT_EQ(2, allDetails.size()); - EXPECT_TRUE(std::find_if(allDetails.begin(), allDetails.end(), [] (const EvaluationDetails& details) { - return details.variationId == "fakeId1"; }) != allDetails.end()); - EXPECT_TRUE(std::find_if(allDetails.begin(), allDetails.end(), [] (const EvaluationDetails& details) { - return details.variationId == "fakeId2"; }) != allDetails.end()); + EXPECT_TRUE(std::find_if(allDetails.begin(), allDetails.end(), [] (const EvaluationDetails& details) { + return *details.variationId == "fakeId1"; }) != allDetails.end()); + EXPECT_TRUE(std::find_if(allDetails.begin(), allDetails.end(), [] (const EvaluationDetails& details) { + return *details.variationId == "fakeId2"; }) != allDetails.end()); } TEST_F(VariationIdTest, GetAllValueDetailsEmpty) { @@ -124,17 +157,17 @@ TEST_F(VariationIdTest, GetKeyAndValue) { client->forceRefresh(); auto result = client->getKeyAndValue("fakeId2"); - EXPECT_TRUE(result != nullptr); + EXPECT_TRUE(result.has_value()); EXPECT_EQ("key2", result->key); EXPECT_FALSE(std::get(result->value)); result = client->getKeyAndValue("percentageId2"); - EXPECT_TRUE(result != nullptr); + EXPECT_TRUE(result.has_value()); EXPECT_EQ("key1", result->key); EXPECT_FALSE(std::get(result->value)); result = client->getKeyAndValue("rolloutId2"); - EXPECT_TRUE(result != nullptr); + EXPECT_TRUE(result.has_value()); EXPECT_EQ("key1", result->key); EXPECT_FALSE(std::get(result->value)); } @@ -145,5 +178,5 @@ TEST_F(VariationIdTest, GetKeyAndValueNotFound) { client->forceRefresh(); auto result = client->getKeyAndValue("nonexisting"); - EXPECT_TRUE(result == nullptr); + EXPECT_FALSE(result.has_value()); }