diff --git a/.circleci/config.yml b/.circleci/config.yml index 4765df71..f248d011 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,13 +27,10 @@ workflows: requires: - build-all - test-netcore-linux: - # .NET Core 2.0 is not a supported platform, but it is the simplest way to test the - # .NET Standard 2.0 target-- since a .NET Core 2.1 application would use the .NET - # Core 2.1 target instead. - name: .NET Standard 2.0 + .NET Core 2.0 - Linux - docker-image: microsoft/dotnet:2.0-sdk-jessie - build-target-framework: netstandard2.0 - test-target-framework: netcoreapp2.0 + name: .NET 6.0 - Linux + docker-image: mcr.microsoft.com/dotnet/sdk:6.0-focal + build-target-framework: net5.0 + test-target-framework: net6.0 requires: - build-all - test-windows: diff --git a/.ldrelease/config.yml b/.ldrelease/config.yml index 83297cda..da1f70b1 100644 --- a/.ldrelease/config.yml +++ b/.ldrelease/config.yml @@ -1,3 +1,5 @@ +version: 2 + repo: public: dotnet-server-sdk private: dotnet-server-sdk-private @@ -6,26 +8,24 @@ publications: - url: https://nuget.org/packages/LaunchDarkly.ServerSdk description: NuGet -circleci: - windows: - context: org-global - -template: - name: dotnet-windows - env: - # See Releaser docs - this causes the generated documentation to include all public APIs from CommonSdk - LD_RELEASE_DOCS_ASSEMBLIES: LaunchDarkly.ServerSdk LaunchDarkly.CommonSdk - LD_RELEASE_DOCS_TARGET_FRAMEWORK: net452 - LD_RELEASE_TEST_TARGET_FRAMEWORK: net452 +jobs: + - docker: {} + template: + name: dotnet-linux + env: + # See Releaser docs - this causes the generated documentation to include all public APIs from CommonSdk + LD_RELEASE_DOCS_ASSEMBLIES: LaunchDarkly.ServerSdk LaunchDarkly.CommonSdk + LD_RELEASE_DOCS_TARGET_FRAMEWORK: netstandard2.0 + LD_RELEASE_TEST_TARGET_FRAMEWORK: net5.0 -releasableBranches: +branches: - name: master description: 6.x - name: 5.x documentation: title: LaunchDarkly Server-Side SDK for .NET - githubPages: true + gitHubPages: true sdk: displayName: ".NET" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6abef41a..fc77303c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -287,7 +287,7 @@ The NuGet package name and assembly name will also change. In the 5.6.3 release, ## [5.2.0] - 2018-07-27 ### Added: -- New configuration property `UseLdd` allows the client to use the "LaunchDarkly Daemon", i.e. getting feature flag data from a store that is updated by an [`ld-relay`](https://docs.launchdarkly.com/docs/the-relay-proxy) instance. However, this will not be usable until the Redis feature store integration is released (soon). +- New configuration property `UseLdd` allows the client to use the "LaunchDarkly Daemon", i.e. getting feature flag data from a store that is updated by an [`ld-relay`](https://docs.launchdarkly.com/home/relay-proxy) instance. However, this will not be usable until the Redis feature store integration is released (soon). ### Changed: - If you attempt to evaluate a flag before the client has established a connection, but you are using a feature store that has already been populated, the client will now use the last known values from the store instead of returning default values. @@ -319,7 +319,7 @@ The NuGet package name and assembly name will also change. In the 5.6.3 release, ## [5.0.0] - 2018-05-10 ### Changed: -- To reduce the network bandwidth used for analytics events, feature request events are now sent as counters rather than individual events, and user details are now sent only at intervals rather than in each event. These behaviors can be modified through the LaunchDarkly UI and with the new configuration option `InlineUsersInEvents`. For more details, see [Analytics Data Stream Reference](https://docs.launchdarkly.com/v2.0/docs/analytics-data-stream-reference). +- To reduce the network bandwidth used for analytics events, feature request events are now sent as counters rather than individual events, and user details are now sent only at intervals rather than in each event. These behaviors can be modified through the LaunchDarkly UI and with the new configuration option `InlineUsersInEvents`. - The `IStoreEvents` interface has been renamed to `IEventProcessor`, has slightly different methods, and includes `IDisposable`. Also, the properties of the `Event` classes have changed. This will only affect developers who created their own implementation of `IStoreEvents`. ### Added: @@ -370,7 +370,7 @@ The NuGet package name and assembly name will also change. In the 5.6.3 release, ## [3.5.0] - 2018-01-29 ### Added -- Support for specifying [private user attributes](https://docs.launchdarkly.com/docs/private-user-attributes) in order to prevent user attributes from being sent in analytics events back to LaunchDarkly. See the `AllAttributesPrivate` and `PrivateAttributeNames` methods on `Configuration` as well as the `AndPrivateX` methods on `User`. +- Support for specifying [private user attributes](https://docs.launchdarkly.com/home/users/attributes#creating-private-user-attributes) in order to prevent user attributes from being sent in analytics events back to LaunchDarkly. See the `AllAttributesPrivate` and `PrivateAttributeNames` methods on `Configuration` as well as the `AndPrivateX` methods on `User`. ### Changed - The stream connection will now restart when a large feature flag update fails repeatedly to ensure that the client is using most recent flag values. @@ -468,7 +468,7 @@ The NuGet package name and assembly name will also change. In the 5.6.3 release, ### Added - Support for multivariate feature flags. New methods `StringVariation`, `JsonVariation` and `IntVariation` and `FloatVariation` for multivariates. - New `AllFlags` method returns all flag values for a specified user. -- New `SecureModeHash` function computes a hash suitable for the new LaunchDarkly [JavaScript client's secure mode feature](https://docs.launchdarkly.com/docs/js-sdk-reference#section-secure-mode). +- New `SecureModeHash` function computes a hash suitable for the new LaunchDarkly [JavaScript client's secure mode feature](https://docs.launchdarkly.com/sdk/features/secure-mode#configuring-secure-mode-in-the-javascript-client-side-sdk). ### Changed - LdClient now implements a new interface: ILdClient diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b1c32f8..fefb3740 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to the LaunchDarkly Server-Side SDK for .NET -LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. +LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/sdk/concepts/contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. ## Submitting bug reports and feature requests diff --git a/README.md b/README.md index 66d90efd..a5540e1c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ For using LaunchDarkly in *client-side* .NET applications, including mobile (Xam ## LaunchDarkly overview -[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) @@ -33,7 +33,7 @@ The only differences in the capabilities of the SDK between platforms are these: ## Getting started -Refer to the [SDK documentation](https://docs.launchdarkly.com/docs/dotnet-sdk-reference#section-getting-started) for instructions on getting started with using the SDK. +Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/server-side/dotnet#getting-started) for instructions on getting started with using the SDK. ## Signing @@ -71,7 +71,7 @@ We encourage pull requests and other contributions from the community. Check out * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. -* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list. +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. * Explore LaunchDarkly * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides diff --git a/docs-src/README.md b/docs-src/README.md index c00a1c7e..cddf9d1e 100644 --- a/docs-src/README.md +++ b/docs-src/README.md @@ -4,7 +4,7 @@ All public types, methods, and properties should have documentation comments in Non-public items may have documentation comments as well, since those may be helpful to other developers working on this project, but they will not be included in the HTML documentation. -The HTML documentation also includes documentation comments from `LaunchDarkly.CommonSdk` (see "Prerequisites" above). These are included automatically when the documentation is built on release. +The HTML documentation also includes documentation comments from `LaunchDarkly.CommonSdk`. These are included automatically when the documentation is built on release, so that developers can see a single unified API in the documentation rather than having to look in two packages. The `docs-src` subdirectory contains additional Markdown content that is included in the documentation build, as follows: diff --git a/src/LaunchDarkly.ServerSdk/Components.cs b/src/LaunchDarkly.ServerSdk/Components.cs index 21e3e471..be62b65f 100644 --- a/src/LaunchDarkly.ServerSdk/Components.cs +++ b/src/LaunchDarkly.ServerSdk/Components.cs @@ -28,7 +28,7 @@ public static class Components /// /// /// "Big segments" are a specific type of user segments. For more information, read the LaunchDarkly - /// documentation about user segments: https://docs.launchdarkly.com/home/users + /// documentation about user segments: https://docs.launchdarkly.com/home/users/segments /// /// /// After configuring this object, use @@ -62,7 +62,7 @@ public static BigSegmentsConfigurationBuilder BigSegments(IBigSegmentStoreFactor /// /// Passing this to causes the SDK /// not to retrieve feature flag data from LaunchDarkly, regardless of any other configuration. This is - /// normally done if you are using the Relay Proxy + /// normally done if you are using the Relay Proxy /// in "daemon mode", where an external process-- the Relay Proxy-- connects to LaunchDarkly and populates /// a persistent data store with the feature flag data. The data store could also be populated by /// another process that is running the LaunchDarkly SDK. If there is no external process updating @@ -122,7 +122,7 @@ public static BigSegmentsConfigurationBuilder BigSegments(IBigSegmentStoreFactor /// is disabled). /// /// - /// For more about how logging works in the SDK, see the SDK + /// For more about how logging works in the SDK, see the SDK /// SDK reference guide. /// /// @@ -159,7 +159,7 @@ public static LoggingConfigurationBuilder Logging() => /// For more about log adapters, see . /// /// - /// For more about how logging works in the SDK, see the SDK + /// For more about how logging works in the SDK, see the SDK /// SDK reference guide. /// /// diff --git a/src/LaunchDarkly.ServerSdk/ConfigurationBuilder.cs b/src/LaunchDarkly.ServerSdk/ConfigurationBuilder.cs index 224c21ba..2ee1c2ba 100644 --- a/src/LaunchDarkly.ServerSdk/ConfigurationBuilder.cs +++ b/src/LaunchDarkly.ServerSdk/ConfigurationBuilder.cs @@ -84,7 +84,7 @@ public Configuration Build() /// /// /// "Big segments" are a specific type of user segments. For more information, read the LaunchDarkly - /// documentation about user segments: https://docs.launchdarkly.com/home/users + /// documentation about user segments: https://docs.launchdarkly.com/home/users/segments /// /// /// If you are using this feature, you will normally specify a database implementation that matches how @@ -226,7 +226,7 @@ public ConfigurationBuilder Http(IHttpConfigurationFactory httpConfigurationFact /// instead. /// /// - /// For more about how logging works in the SDK, see the SDK + /// For more about how logging works in the SDK, see the SDK /// SDK reference guide. /// /// @@ -256,7 +256,7 @@ public ConfigurationBuilder Logging(ILoggingConfigurationFactory loggingConfigur /// only want to specify the basic logging destination, and do not need to set other log properties. /// /// - /// For more about how logging works in the SDK, see the SDK + /// For more about how logging works in the SDK, see the SDK /// SDK reference guide. /// /// diff --git a/src/LaunchDarkly.ServerSdk/Integrations/BigSegmentsConfigurationBuilder.cs b/src/LaunchDarkly.ServerSdk/Integrations/BigSegmentsConfigurationBuilder.cs index 07f9cf17..eaf40c9d 100644 --- a/src/LaunchDarkly.ServerSdk/Integrations/BigSegmentsConfigurationBuilder.cs +++ b/src/LaunchDarkly.ServerSdk/Integrations/BigSegmentsConfigurationBuilder.cs @@ -9,7 +9,7 @@ namespace LaunchDarkly.Sdk.Server.Integrations /// /// /// "Big segments" are a specific type of user segments. For more information, read the LaunchDarkly - /// documentation about user segments: https://docs.launchdarkly.com/home/users + /// documentation about user segments: https://docs.launchdarkly.com/home/users/segments /// /// /// If you want to set non-default values for any of these properties, create a builder with diff --git a/src/LaunchDarkly.ServerSdk/Integrations/EventProcessorBuilder.cs b/src/LaunchDarkly.ServerSdk/Integrations/EventProcessorBuilder.cs index 032ebe8f..58cf1df8 100644 --- a/src/LaunchDarkly.ServerSdk/Integrations/EventProcessorBuilder.cs +++ b/src/LaunchDarkly.ServerSdk/Integrations/EventProcessorBuilder.cs @@ -93,17 +93,13 @@ public EventProcessorBuilder AllAttributesPrivate(bool allAttributesPrivate) /// /// You will only need to change this value in the following cases: /// - /// - /// - /// You are using the Relay Proxy. + /// + /// You are using the Relay Proxy. /// Set BaseUri to the base URI of the Relay Proxy instance. - /// - /// - /// - /// + /// + /// /// You are connecting to a test server or a nonstandard endpoint for the LaunchDarkly service. - /// - /// + /// /// /// /// the base URI of the events service; null to use the default diff --git a/src/LaunchDarkly.ServerSdk/Integrations/FileData.cs b/src/LaunchDarkly.ServerSdk/Integrations/FileData.cs index aff039d9..367159bc 100644 --- a/src/LaunchDarkly.ServerSdk/Integrations/FileData.cs +++ b/src/LaunchDarkly.ServerSdk/Integrations/FileData.cs @@ -49,9 +49,9 @@ public static class FileData /// contain an object with three possible properties: /// /// - /// flags: Feature flag definitions. - /// flagVersions: Simplified feature flags that contain only a value. - /// segments: User segment definitions. + /// flags: Feature flag definitions. + /// flagVersions: Simplified feature flags that contain only a value. + /// segments: User segment definitions. /// /// /// The format of the data in flags and segments is defined by the LaunchDarkly application diff --git a/src/LaunchDarkly.ServerSdk/Integrations/LoggingConfigurationBuilder.cs b/src/LaunchDarkly.ServerSdk/Integrations/LoggingConfigurationBuilder.cs index acea3602..643621bc 100644 --- a/src/LaunchDarkly.ServerSdk/Integrations/LoggingConfigurationBuilder.cs +++ b/src/LaunchDarkly.ServerSdk/Integrations/LoggingConfigurationBuilder.cs @@ -17,12 +17,13 @@ namespace LaunchDarkly.Sdk.Server.Integrations /// By default, the SDK has the following logging behavior: /// /// - /// Log messages are written to standard output. To change this, use a log adapter as - /// described in and . - /// The lowest enabled log level is , so - /// messages are not shown. To change this, use . - /// The base logger name is LaunchDarkly.Sdk. See - /// for more about logger names and how to change the name. + /// Log messages are written to standard output. To change this, use a log adapter as + /// described in and . + /// The lowest enabled log level is , + /// so messages are not shown. To change this, use + /// . + /// The base logger name is LaunchDarkly.Sdk. See + /// for more about logger names and how to change the name. /// /// /// @@ -73,15 +74,15 @@ public LoggingConfigurationBuilder() { } /// functionality is involved: /// /// - /// .DataSource: problems or status messages regarding how the SDK gets - /// feature flag data from LaunchDarkly. - /// .DataStore: problems or status messages regarding how the SDK stores its - /// feature flag data (for instance, if you are using a database). - /// .Evaluation: problems in evaluating a feature flag or flags, which were + /// .DataSource: problems or status messages regarding how the SDK gets + /// feature flag data from LaunchDarkly. + /// .DataStore: problems or status messages regarding how the SDK stores its + /// feature flag data (for instance, if you are using a database). + /// .Evaluation: problems in evaluating a feature flag or flags, which were /// caused by invalid flag data or incorrect usage of the SDK rather than for instance a - /// database problem. - /// .Events problems or status messages regarding the SDK's delivery of - /// analytics event data to LaunchDarkly. + /// database problem. + /// .Events problems or status messages regarding the SDK's delivery of + /// analytics event data to LaunchDarkly. /// /// /// Setting BaseLoggerName to a non-null value overrides the default. The SDK still @@ -107,7 +108,7 @@ public LoggingConfigurationBuilder BaseLoggerName(string baseLoggerName) /// For instance, in .NET Core, specify Logs.CoreLogging to use the standard .NET Core logging framework. /// /// - /// For more about logging adapters, see the SDK + /// For more about logging adapters, see the SDK /// reference guide, the API /// documentation for LaunchDarkly.Logging, and the /// third-party adapters that @@ -143,7 +144,7 @@ public LoggingConfigurationBuilder Adapter(ILogAdapter adapter) /// /// This adds a log level filter that is applied regardless of what implementation of logging is /// being used, so that log messages at lower levels are suppressed. For instance, setting the - /// minimum level to means that Debug-level output is disabled. + /// minimum level to means that Debug-level output is disabled. /// External logging frameworks may also have their own mechanisms for setting a minimum log level. /// /// diff --git a/src/LaunchDarkly.ServerSdk/Integrations/PollingDataSourceBuilder.cs b/src/LaunchDarkly.ServerSdk/Integrations/PollingDataSourceBuilder.cs index 54fef212..e3ce7643 100644 --- a/src/LaunchDarkly.ServerSdk/Integrations/PollingDataSourceBuilder.cs +++ b/src/LaunchDarkly.ServerSdk/Integrations/PollingDataSourceBuilder.cs @@ -49,17 +49,13 @@ public sealed class PollingDataSourceBuilder : IDataSourceFactory, IDiagnosticDe /// /// You will only need to change this value in the following cases: /// - /// - /// - /// You are using the Relay Proxy. + /// + /// You are using the Relay Proxy. /// Set BaseUri to the base URI of the Relay Proxy instance. - /// - /// - /// - /// + /// + /// /// You are connecting to a test server or a nonstandard endpoint for the LaunchDarkly service. - /// - /// + /// /// /// /// the base URI of the polling service; null to use the default diff --git a/src/LaunchDarkly.ServerSdk/Integrations/StreamingDataSourceBuilder.cs b/src/LaunchDarkly.ServerSdk/Integrations/StreamingDataSourceBuilder.cs index a48dc5fd..1aec214c 100644 --- a/src/LaunchDarkly.ServerSdk/Integrations/StreamingDataSourceBuilder.cs +++ b/src/LaunchDarkly.ServerSdk/Integrations/StreamingDataSourceBuilder.cs @@ -46,17 +46,13 @@ public sealed class StreamingDataSourceBuilder : IDataSourceFactory, IDiagnostic /// /// You will only need to change this value in the following cases: /// - /// - /// - /// You are using the Relay Proxy. + /// + /// You are using the Relay Proxy. /// Set BaseUri to the base URI of the Relay Proxy instance. - /// - /// - /// - /// + /// + /// /// You are connecting to a test server or a nonstandard endpoint for the LaunchDarkly service. - /// - /// + /// /// /// /// the base URI of the streaming service; null to use the default diff --git a/src/LaunchDarkly.ServerSdk/Interfaces/BigSegmentStoreStatus.cs b/src/LaunchDarkly.ServerSdk/Interfaces/BigSegmentStoreStatus.cs index e353075c..033b4f9c 100644 --- a/src/LaunchDarkly.ServerSdk/Interfaces/BigSegmentStoreStatus.cs +++ b/src/LaunchDarkly.ServerSdk/Interfaces/BigSegmentStoreStatus.cs @@ -8,7 +8,7 @@ namespace LaunchDarkly.Sdk.Server.Interfaces /// /// /// "Big segments" are a specific type of user segments. For more information, read the LaunchDarkly - /// documentation about user segments: https://docs.launchdarkly.com/home/users + /// documentation about user segments: https://docs.launchdarkly.com/home/users/segments /// public struct BigSegmentStoreStatus { diff --git a/src/LaunchDarkly.ServerSdk/Interfaces/BigSegmentsConfiguration.cs b/src/LaunchDarkly.ServerSdk/Interfaces/BigSegmentsConfiguration.cs index a68f141f..5ce011cd 100644 --- a/src/LaunchDarkly.ServerSdk/Interfaces/BigSegmentsConfiguration.cs +++ b/src/LaunchDarkly.ServerSdk/Interfaces/BigSegmentsConfiguration.cs @@ -9,7 +9,7 @@ namespace LaunchDarkly.Sdk.Server.Interfaces /// /// /// "Big segments" are a specific type of user segments. For more information, read the LaunchDarkly - /// documentation about user segments: https://docs.launchdarkly.com/home/users + /// documentation about user segments: https://docs.launchdarkly.com/home/users/segments /// /// /// See for more details on these properties. diff --git a/src/LaunchDarkly.ServerSdk/Interfaces/DataSourceStatus.cs b/src/LaunchDarkly.ServerSdk/Interfaces/DataSourceStatus.cs index f7ab6fd7..4fe980b2 100644 --- a/src/LaunchDarkly.ServerSdk/Interfaces/DataSourceStatus.cs +++ b/src/LaunchDarkly.ServerSdk/Interfaces/DataSourceStatus.cs @@ -22,14 +22,14 @@ public struct DataSourceStatus /// /// The meaning of this depends on the current state: /// - /// For , it is the time that the SDK started initializing. - /// For , it is the time that the data source most recently entered a valid + /// For , it is the time that the SDK started initializing. + /// For , it is the time that the data source most recently entered a valid /// state, after previously having been either or - /// . - /// For , it is the time that the data source most recently entered an - /// error state, after previously having been . - /// For , it is the time that the data source encountered an unrecoverable error - /// or that the SDK was explicitly shut down. + /// . + /// For , it is the time that the data source most recently entered an + /// error state, after previously having been . + /// For , it is the time that the data source encountered an unrecoverable error + /// or that the SDK was explicitly shut down. /// /// public DateTime StateSince { get; set; } @@ -47,6 +47,10 @@ public struct DataSourceStatus /// public ErrorInfo? LastError { get; set; } + /// + public override string ToString() => + string.Format("DataSourceStatus({0},{1},{2})", State, StateSince, LastError); + /// /// A description of an error condition that the data source encountered. /// diff --git a/src/LaunchDarkly.ServerSdk/Interfaces/DataStoreStatus.cs b/src/LaunchDarkly.ServerSdk/Interfaces/DataStoreStatus.cs index 14e279c7..db1601c1 100644 --- a/src/LaunchDarkly.ServerSdk/Interfaces/DataStoreStatus.cs +++ b/src/LaunchDarkly.ServerSdk/Interfaces/DataStoreStatus.cs @@ -26,5 +26,9 @@ public struct DataStoreStatus /// This property is not meaningful to application code. It is used internally. /// public bool RefreshNeeded { get; set; } + + /// + public override string ToString() => + string.Format("DataStoreStatus({0},{1})", Available, RefreshNeeded); } } diff --git a/src/LaunchDarkly.ServerSdk/Interfaces/IBigSegmentStore.cs b/src/LaunchDarkly.ServerSdk/Interfaces/IBigSegmentStore.cs index d02bbcc1..fa397f95 100644 --- a/src/LaunchDarkly.ServerSdk/Interfaces/IBigSegmentStore.cs +++ b/src/LaunchDarkly.ServerSdk/Interfaces/IBigSegmentStore.cs @@ -9,7 +9,7 @@ namespace LaunchDarkly.Sdk.Server.Interfaces /// /// /// "Big segments" are a specific type of user segments. For more information, read the LaunchDarkly - /// documentation about user segments: https://docs.launchdarkly.com/home/users + /// documentation about user segments: https://docs.launchdarkly.com/home/users/segments /// /// /// All query methods of the store are asynchronous. diff --git a/src/LaunchDarkly.ServerSdk/Interfaces/IBigSegmentStoreStatusProvider.cs b/src/LaunchDarkly.ServerSdk/Interfaces/IBigSegmentStoreStatusProvider.cs index 302bf7f9..db3a4c77 100644 --- a/src/LaunchDarkly.ServerSdk/Interfaces/IBigSegmentStoreStatusProvider.cs +++ b/src/LaunchDarkly.ServerSdk/Interfaces/IBigSegmentStoreStatusProvider.cs @@ -10,7 +10,7 @@ namespace LaunchDarkly.Sdk.Server.Interfaces /// The big segment store is the component that receives information about big segments, normally /// from a database populated by the LaunchDarkly Relay Proxy. "Big segments" are a specific type /// of user segments. For more information, read the LaunchDarkly documentation about user - /// segments: https://docs.launchdarkly.com/home/users + /// segments: https://docs.launchdarkly.com/home/users/segments /// /// /// An implementation of this interface is returned by . diff --git a/src/LaunchDarkly.ServerSdk/Interfaces/ILdClient.cs b/src/LaunchDarkly.ServerSdk/Interfaces/ILdClient.cs index a2e575d9..62f79537 100644 --- a/src/LaunchDarkly.ServerSdk/Interfaces/ILdClient.cs +++ b/src/LaunchDarkly.ServerSdk/Interfaces/ILdClient.cs @@ -352,7 +352,7 @@ public interface ILdClient /// parameter. As a result, calling this overload of Track will not yet produce any different /// behavior from calling without a metricValue. Refer /// to the SDK reference guide for the latest status: - /// https://docs.launchdarkly.com/docs/dotnet-sdk-reference#section-track + /// https://docs.launchdarkly.com/sdk/features/events#net /// /// the name of the event /// the user that performed the event @@ -401,7 +401,7 @@ public interface ILdClient /// /// The object returned by this method contains the flag values as well as other metadata that /// is used by the LaunchDarkly JavaScript client, so it can be used for - /// bootstrapping. + /// bootstrapping. /// /// /// This method will not send analytics events back to LaunchDarkly. @@ -418,7 +418,7 @@ public interface ILdClient /// Creates a hash string that can be used by the JavaScript SDK to identify a user. /// /// - /// See Secure mode in + /// See Secure mode in /// the JavaScript SDK Reference. /// /// the user to be hashed along with the SDK key diff --git a/src/LaunchDarkly.ServerSdk/Interfaces/IPersistentDataStore.cs b/src/LaunchDarkly.ServerSdk/Interfaces/IPersistentDataStore.cs index 4315b6c7..5232c1f0 100644 --- a/src/LaunchDarkly.ServerSdk/Interfaces/IPersistentDataStore.cs +++ b/src/LaunchDarkly.ServerSdk/Interfaces/IPersistentDataStore.cs @@ -34,15 +34,15 @@ namespace LaunchDarkly.Sdk.Server.Interfaces /// implementation can use for persisting this data: /// /// - /// + /// /// Preferably, it should store the version number and the /// state separately so that the object does not need to be fully deserialized to read /// them. In this case, deleted item placeholders can ignore the value of /// on writes and can set it to /// null on reads. The store should never call /// or in this case. - /// - /// + /// + /// /// If that isn't possible, then the store should simply persist the exact string from /// on writes, and return the persisted /// string on reads -- setting to zero and @@ -50,7 +50,7 @@ namespace LaunchDarkly.Sdk.Server.Interfaces /// provide the SDK with enough information to infer the version and the deleted state. /// On updates, the store will have to call in /// order to inspect the version number of the existing item if any. - /// + /// /// /// /// Error handling is defined as follows: if any data store operation encounters a database @@ -91,17 +91,17 @@ public interface IPersistentDataStore : IDisposable /// a as follows: /// /// - /// + /// /// If the version number and deletion state can be determined without fully deserializing /// the item, then the store should set those properties in the /// (and can set to null for deleted items). - /// - /// + /// + /// /// Otherwise, it should simply set to /// the exact string that was persisted, and can leave the other properties as zero/false. The /// SDK will inspect the properties of the item after deserializing it to fill in the rest of /// the information. - /// + /// /// /// /// specifies which collection to use diff --git a/src/LaunchDarkly.ServerSdk/Interfaces/LdClientContext.cs b/src/LaunchDarkly.ServerSdk/Interfaces/LdClientContext.cs index 992bb495..75f68322 100644 --- a/src/LaunchDarkly.ServerSdk/Interfaces/LdClientContext.cs +++ b/src/LaunchDarkly.ServerSdk/Interfaces/LdClientContext.cs @@ -1,6 +1,6 @@ using LaunchDarkly.Logging; +using LaunchDarkly.Sdk.Internal; using LaunchDarkly.Sdk.Internal.Events; -using LaunchDarkly.Sdk.Server.Internal; namespace LaunchDarkly.Sdk.Server.Interfaces { @@ -45,7 +45,7 @@ Configuration configuration basic, (configuration.HttpConfigurationFactory ?? Components.HttpConfiguration()).CreateHttpConfiguration(basic), null, - new TaskExecutor(Logs.None.Logger("")) + new TaskExecutor("test-sender", Logs.None.Logger("")) ) { } internal LdClientContext( diff --git a/src/LaunchDarkly.ServerSdk/Internal/BigSegments/BigSegmentStoreWrapper.cs b/src/LaunchDarkly.ServerSdk/Internal/BigSegments/BigSegmentStoreWrapper.cs index 177b3a82..1ede470c 100644 --- a/src/LaunchDarkly.ServerSdk/Internal/BigSegments/BigSegmentStoreWrapper.cs +++ b/src/LaunchDarkly.ServerSdk/Internal/BigSegments/BigSegmentStoreWrapper.cs @@ -4,6 +4,7 @@ using LaunchDarkly.Cache; using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Internal; +using LaunchDarkly.Sdk.Internal.Concurrent; using LaunchDarkly.Sdk.Server.Interfaces; using static LaunchDarkly.Sdk.Server.Interfaces.BigSegmentStoreTypes; @@ -155,7 +156,7 @@ private async Task PollStoreAndUpdateStatusAsync() if (!oldStatus.HasValue || !newStatus.Equals(oldStatus.Value)) { _logger.Debug("Big segment store status changed from {0} to {1}", oldStatus, newStatus); - _taskExecutor.ScheduleEvent(this, newStatus, StatusChanged); + _taskExecutor.ScheduleEvent(newStatus, StatusChanged); } return newStatus; diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataSources/DataSourceStatusProviderImpl.cs b/src/LaunchDarkly.ServerSdk/Internal/DataSources/DataSourceStatusProviderImpl.cs index f9196722..b29d0d93 100644 --- a/src/LaunchDarkly.ServerSdk/Internal/DataSources/DataSourceStatusProviderImpl.cs +++ b/src/LaunchDarkly.ServerSdk/Internal/DataSources/DataSourceStatusProviderImpl.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using LaunchDarkly.Sdk.Internal; +using LaunchDarkly.Sdk.Internal.Concurrent; using LaunchDarkly.Sdk.Server.Interfaces; namespace LaunchDarkly.Sdk.Server.Internal.DataSources diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataSources/DataSourceUpdatesImpl.cs b/src/LaunchDarkly.ServerSdk/Internal/DataSources/DataSourceUpdatesImpl.cs index a13e455b..70be5c2f 100644 --- a/src/LaunchDarkly.ServerSdk/Internal/DataSources/DataSourceUpdatesImpl.cs +++ b/src/LaunchDarkly.ServerSdk/Internal/DataSources/DataSourceUpdatesImpl.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Internal; +using LaunchDarkly.Sdk.Internal.Concurrent; using LaunchDarkly.Sdk.Server.Interfaces; using LaunchDarkly.Sdk.Server.Internal.DataStores; @@ -27,13 +28,11 @@ internal sealed class DataSourceUpdatesImpl : IDataSourceUpdates private readonly IDataStore _store; private readonly IDataStoreStatusProvider _dataStoreStatusProvider; private readonly TaskExecutor _taskExecutor; + private readonly StateMonitor _status; private readonly Logger _log; private readonly DependencyTracker _dependencyTracker; private readonly DataSourceOutageTracker _outageTracker; - private readonly MultiNotifier _stateChangedSignal = new MultiNotifier(); - private readonly object _stateLock = new object(); - private DataSourceStatus _currentStatus; private volatile bool _lastStoreUpdateFailed = false; #endregion @@ -46,16 +45,7 @@ internal sealed class DataSourceUpdatesImpl : IDataSourceUpdates #region Internal properties - internal DataSourceStatus LastStatus - { - get - { - lock (_stateLock) - { - return _currentStatus; - } - } - } + internal DataSourceStatus LastStatus => _status.Current; #endregion @@ -87,12 +77,13 @@ internal DataSourceUpdatesImpl( _outageTracker = outageLoggingTimeout.HasValue ? new DataSourceOutageTracker(_log, outageLoggingTimeout.Value) : null; - _currentStatus = new DataSourceStatus + var initialStatus = new DataSourceStatus { State = DataSourceState.Initializing, StateSince = DateTime.Now, LastError = null }; + _status = new StateMonitor(initialStatus, MaybeUpdateStatus, _log); } #endregion @@ -169,37 +160,43 @@ public bool Upsert(DataKind kind, string key, ItemDescriptor item) return true; } - public void UpdateStatus(DataSourceState newState, DataSourceStatus.ErrorInfo? newError) + private struct StateAndError { - DataSourceStatus? statusToBroadcast = null; - - lock (_stateLock) - { - var oldStatus = _currentStatus; + public DataSourceState State { get; set; } + public DataSourceStatus.ErrorInfo? Error { get; set; } + } - if (newState == DataSourceState.Interrupted && oldStatus.State == DataSourceState.Initializing) - { - newState = DataSourceState.Initializing; // see comment on IDataSourceUpdates.UpdateStatus - } + private static DataSourceStatus? MaybeUpdateStatus( + DataSourceStatus oldStatus, + StateAndError update + ) + { + var newState = + (update.State == DataSourceState.Interrupted && oldStatus.State == DataSourceState.Initializing) + ? DataSourceState.Initializing // see comment on IDataSourceUpdates.UpdateStatus + : update.State; - if (newState != oldStatus.State || newError.HasValue) - { - _currentStatus = new DataSourceStatus - { - State = newState, - StateSince = newState == oldStatus.State ? oldStatus.StateSince : DateTime.Now, - LastError = newError.HasValue ? newError : oldStatus.LastError - }; - statusToBroadcast = _currentStatus; - _stateChangedSignal.NotifyAll(); - } + if (newState == oldStatus.State && !update.Error.HasValue) + { + return null; } + return new DataSourceStatus + { + State = newState, + StateSince = newState == oldStatus.State ? oldStatus.StateSince : DateTime.Now, + LastError = update.Error ?? oldStatus.LastError + }; + } - _outageTracker?.TrackDataSourceState(newState, newError); + public void UpdateStatus(DataSourceState newState, DataSourceStatus.ErrorInfo? newError) + { + var updated = _status.Update(new StateAndError { State = newState, Error = newError }, + out var newStatus); - if (statusToBroadcast.HasValue) + if (updated) { - _taskExecutor.ScheduleEvent(this, statusToBroadcast.Value, StatusChanged); + _outageTracker?.TrackDataSourceState(newStatus.State, newError); + _taskExecutor.ScheduleEvent(newStatus, StatusChanged); } } @@ -209,7 +206,7 @@ public void UpdateStatus(DataSourceState newState, DataSourceStatus.ErrorInfo? n public void Dispose() { - _stateChangedSignal.Dispose(); + _status.Dispose(); } #endregion @@ -218,45 +215,11 @@ public void Dispose() internal async Task WaitForAsync(DataSourceState desiredState, TimeSpan timeout) { - var deadline = DateTime.Now.Add(timeout); - bool hasTimeout = timeout.CompareTo(TimeSpan.Zero) > 0; - - while (true) - { - MultiNotifierToken stateAwaiter; - TimeSpan timeToWait; - lock (_stateLock) - { - if (_currentStatus.State == desiredState) - { - return true; - } - if (_currentStatus.State == DataSourceState.Off) - { - return false; - } - - // Here we're using a slightly roundabout mechanism to keep track of however many tasks might - // be simultaneously waiting on WaitForAsync, because .NET doesn't have an async concurrency - // primitive equivalent to Java's wait/notifyAll(). What we're creating here is a cancellation - // token that will be cancelled (by UpdateStatus) the next time the status is changed in any - // way. - if (hasTimeout) - { - timeToWait = deadline.Subtract(DateTime.Now); - if (timeToWait.CompareTo(TimeSpan.Zero) <= 0) - { - return false; - } - } - else - { - timeToWait = TimeSpan.FromMilliseconds(-1); // special value makes Task.Delay wait indefinitely - } - stateAwaiter = _stateChangedSignal.Token; - } - await stateAwaiter.WaitAsync(timeToWait); - } + var newStatus = await _status.WaitForAsync( + status => status.State == desiredState || status.State == DataSourceState.Off, + timeout + ); + return newStatus.HasValue && newStatus.Value.State == desiredState; } #endregion @@ -278,7 +241,7 @@ private void SendChangeEvents(IEnumerable affectedItems) if (item.Kind == DataModel.Features) { var eventArgs = new FlagChangeEvent(item.Key); - _taskExecutor.ScheduleEvent(this, eventArgs, copyOfHandlers); + _taskExecutor.ScheduleEvent(eventArgs, copyOfHandlers); } } } diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FileDataSource.cs b/src/LaunchDarkly.ServerSdk/Internal/DataSources/FileDataSource.cs index 72354337..ebea2b35 100644 --- a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FileDataSource.cs +++ b/src/LaunchDarkly.ServerSdk/Internal/DataSources/FileDataSource.cs @@ -8,6 +8,7 @@ using LaunchDarkly.Sdk.Internal; using LaunchDarkly.Sdk.Server.Integrations; using LaunchDarkly.Sdk.Server.Interfaces; +using LaunchDarkly.Sdk.Server.Internal.Model; using static LaunchDarkly.Sdk.Server.Interfaces.DataStoreTypes; @@ -25,6 +26,7 @@ internal sealed class FileDataSource : IDataSource private readonly Logger _logger; private volatile bool _started; private volatile bool _loadedValidData; + private volatile int _lastVersion; public FileDataSource(IDataSourceUpdates dataSourceUpdates, FileDataTypes.IFileReader fileReader, List paths, bool autoUpdate, Func alternateParser, bool skipMissingPaths, @@ -38,6 +40,7 @@ public FileDataSource(IDataSourceUpdates dataSourceUpdates, FileDataTypes.IFileR _dataMerger = new FlagFileDataMerger(duplicateKeysHandling); _fileReader = fileReader; _skipMissingPaths = skipMissingPaths; + _lastVersion = 0; if (autoUpdate) { try @@ -86,6 +89,7 @@ private void Dispose(bool disposing) private void LoadAll() { + var version = Interlocked.Increment(ref _lastVersion); var flags = new Dictionary(); var segments = new Dictionary(); foreach (var path in _paths) @@ -93,7 +97,8 @@ private void LoadAll() try { var content = _fileReader.ReadAllText(path); - var data = _parser.Parse(content); + _logger.Debug("file data: {0}", content); + var data = _parser.Parse(content, version); _dataMerger.AddToData(data, flags, segments); } catch (FileNotFoundException) when (_skipMissingPaths) @@ -115,55 +120,120 @@ private void LoadAll() _loadedValidData = true; } - private const int ReadFileRetryDelay = 200; - private const int ReadFileRetryAttempts = 30000 / ReadFileRetryDelay; + private void TriggerReload() + { + if (_started) + { + _logger.Info("detected file modification, reloading"); + LoadAll(); + } + } + } + + // Provides the logic for merging sets of feature flag and segment data. + internal sealed class FlagFileDataMerger + { + private readonly FileDataTypes.DuplicateKeysHandling _duplicateKeysHandling; + + public FlagFileDataMerger(FileDataTypes.DuplicateKeysHandling duplicateKeysHandling) + { + _duplicateKeysHandling = duplicateKeysHandling; + } - private static string ReadFileContent(string path) + public void AddToData( + FullDataSet data, + IDictionary flagsOut, + IDictionary segmentsOut + ) { - int delay = 0; - for (int i = 0; ; i++) + foreach (var kv0 in data.Data) { - try + var kind = kv0.Key; + foreach (var kv1 in kv0.Value.Items) { - string content = File.ReadAllText(path); - return content; - } - catch (IOException e) when (IsFileLocked(e)) - { - // Retry for approximately 30 seconds before throwing - if (i > ReadFileRetryAttempts) + var items = kind == DataModel.Segments ? segmentsOut : flagsOut; + var key = kv1.Key; + var item = kv1.Value; + if (items.ContainsKey(key)) { - throw; + switch (_duplicateKeysHandling) + { + case FileDataTypes.DuplicateKeysHandling.Throw: + throw new System.Exception("in \"" + kind.Name + "\", key \"" + key + + "\" was already defined"); + case FileDataTypes.DuplicateKeysHandling.Ignore: + break; + default: + throw new NotImplementedException("Unknown duplicate keys handling: " + _duplicateKeysHandling); + } + } + else + { + items[key] = item; } - Thread.Sleep(delay); - // Retry immediately the first time but 200ms thereafter - delay = ReadFileRetryDelay; } } } + } + + /// + /// Implementation of file monitoring using FileSystemWatcher. + /// + internal sealed class FileWatchingReloader : IDisposable + { + private readonly ISet _filePaths; + private readonly Action _reload; + private readonly List _watchers; + + public FileWatchingReloader(List paths, Action reload) + { + _reload = reload; + + _filePaths = new HashSet(); + var dirPaths = new HashSet(); + foreach (var p in paths) + { + var absPath = Path.GetFullPath(p); + _filePaths.Add(absPath); + var dirPath = Path.GetDirectoryName(absPath); + dirPaths.Add(dirPath); + } + + _watchers = new List(); + foreach (var dir in dirPaths) + { + var w = new FileSystemWatcher(dir); + + w.Changed += (s, args) => ChangedPath(args.FullPath); + w.Created += (s, args) => ChangedPath(args.FullPath); + w.Renamed += (s, args) => ChangedPath(args.FullPath); + w.EnableRaisingEvents = true; + + _watchers.Add(w); + } + } - private static bool IsFileLocked(IOException exception) + private void ChangedPath(string path) { - // We cannot guarantee that these HResult values will be present on non-Windows OSes. However, this - // logic is less important on other platforms, because in Unix-like OSes you can atomically replace a - // file's contents (by creating a temporary file and then renaming it to overwrite the original file), - // so FileDataSource will not try to read an incomplete update; that is not possibble in Windows. - int errorCode = exception.HResult & 0xffff; - switch (errorCode) + if (_filePaths.Contains(path)) { - case 0x20: // ERROR_SHARING_VIOLATION - case 0x21: // ERROR_LOCK_VIOLATION - return true; - default: - return false; + _reload(); } } - private void TriggerReload() + public void Dispose() { - if (_started) + Dispose(true); + } + + private void Dispose(bool disposing) + { + if (disposing) { - LoadAll(); + foreach (var w in _watchers) + { + w.Dispose(); + } } } } diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FileWatchingReloader.cs b/src/LaunchDarkly.ServerSdk/Internal/DataSources/FileWatchingReloader.cs deleted file mode 100644 index a0faa646..00000000 --- a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FileWatchingReloader.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace LaunchDarkly.Sdk.Server.Internal.DataSources -{ - /// - /// Implementation of file monitoring using FileSystemWatcher. - /// - internal sealed class FileWatchingReloader : IDisposable - { - private readonly ISet _filePaths; - private readonly Action _reload; - private readonly List _watchers; - - public FileWatchingReloader(List paths, Action reload) - { - _reload = reload; - - _filePaths = new HashSet(); - var dirPaths = new HashSet(); - foreach (var p in paths) - { - var absPath = Path.GetFullPath(p); - _filePaths.Add(absPath); - var dirPath = Path.GetDirectoryName(absPath); - dirPaths.Add(dirPath); - } - - _watchers = new List(); - foreach (var dir in dirPaths) - { - var w = new FileSystemWatcher(dir); - - w.Changed += (s, args) => ChangedPath(args.FullPath); - w.Created += (s, args) => ChangedPath(args.FullPath); - w.Renamed += (s, args) => ChangedPath(args.FullPath); - w.EnableRaisingEvents = true; - - _watchers.Add(w); - } - } - - private void ChangedPath(string path) - { - if (_filePaths.Contains(path)) - { - _reload(); - } - } - - public void Dispose() - { - Dispose(true); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - foreach (var w in _watchers) - { - w.Dispose(); - } - } - } - } -} diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFactory.cs b/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFactory.cs deleted file mode 100644 index f4be27ca..00000000 --- a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ - -namespace LaunchDarkly.Sdk.Server.Internal.DataSources -{ - internal static class FlagFactory - { - // Constructs a flag that always returns the same value. This is done by giving it a - // single variation and setting the fallthrough variation to that. - public static object FlagWithValue(string key, LdValue value) - { - var json = LdValue.BuildObject() - .Add("key", key) - .Add("version", 1) - .Add("on", true) - .Add("variations", LdValue.ArrayOf(value)) - .Add("fallthrough", LdValue.BuildObject().Add("variation", 0).Build()) - .Build() - .ToJsonString(); - return DataModel.Features.Deserialize(json).Item; - } - } -} diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFileData.cs b/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFileData.cs deleted file mode 100644 index 4d13c63a..00000000 --- a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFileData.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using LaunchDarkly.Sdk.Server.Internal.Model; - -namespace LaunchDarkly.Sdk.Server.Internal.DataSources -{ - // Represents the data structure that we parse files into, and provides the logic for - // transferring its contents into the format used by the data store. - internal sealed class FlagFileData - { - public Dictionary Flags { get; set; } - - public Dictionary FlagValues { get; set; } - - public Dictionary Segments { get; set; } - } -} diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFileDataMerger.cs b/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFileDataMerger.cs deleted file mode 100644 index 1883b6a1..00000000 --- a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFileDataMerger.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using LaunchDarkly.Sdk.Server.Integrations; -using LaunchDarkly.Sdk.Server.Internal.Model; - -using static LaunchDarkly.Sdk.Server.Interfaces.DataStoreTypes; - -namespace LaunchDarkly.Sdk.Server.Internal.DataSources -{ - // Provides the logic for merging sets of feature flag and segment data. - internal sealed class FlagFileDataMerger - { - private readonly FileDataTypes.DuplicateKeysHandling _duplicateKeysHandling; - - public FlagFileDataMerger(FileDataTypes.DuplicateKeysHandling duplicateKeysHandling) - { - _duplicateKeysHandling = duplicateKeysHandling; - } - - public void AddToData(FlagFileData data, IDictionary flagsOut, IDictionary segmentsOut) - { - if (data.Flags != null) - { - foreach (KeyValuePair e in data.Flags) - { - AddItem(DataModel.Features, flagsOut, e.Key, e.Value); - } - } - if (data.FlagValues != null) - { - foreach (KeyValuePair e in data.FlagValues) - { - AddItem(DataModel.Features, flagsOut, e.Key, FlagFactory.FlagWithValue(e.Key, e.Value)); - } - } - if (data.Segments != null) - { - foreach (KeyValuePair e in data.Segments) - { - AddItem(DataModel.Segments, segmentsOut, e.Key, e.Value); - } - } - } - - private void AddItem(DataKind kind, IDictionary items, string key, object item) - { - if (items.ContainsKey(key)) - { - switch (_duplicateKeysHandling) - { - case FileDataTypes.DuplicateKeysHandling.Throw: - throw new System.Exception("in \"" + kind.Name + "\", key \"" + key + - "\" was already defined"); - case FileDataTypes.DuplicateKeysHandling.Ignore: - break; - default: - throw new NotImplementedException("Unknown duplicate keys handling: " + _duplicateKeysHandling); - } - } - else - { - items[key] = new ItemDescriptor(1, item); - } - } - } -} diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFileParser.cs b/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFileParser.cs index 7bc861c1..f49a8319 100644 --- a/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFileParser.cs +++ b/src/LaunchDarkly.ServerSdk/Internal/DataSources/FlagFileParser.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using LaunchDarkly.JsonStream; using LaunchDarkly.Sdk.Server.Internal.Model; using static LaunchDarkly.Sdk.Json.LdJsonConverters; +using static LaunchDarkly.Sdk.Server.Interfaces.DataStoreTypes; namespace LaunchDarkly.Sdk.Server.Internal.DataSources { @@ -16,11 +18,11 @@ public FlagFileParser(Func alternateParser) _alternateParser = alternateParser; } - public FlagFileData Parse(string content) + public FullDataSet Parse(string content, int version) { if (_alternateParser == null) { - return ParseJson(content); + return ParseJson(content, version); } else { @@ -28,7 +30,7 @@ public FlagFileData Parse(string content) { try { - return ParseJson(content); + return ParseJson(content, version); } catch (Exception) { @@ -41,24 +43,20 @@ public FlagFileData Parse(string content) // our existing data model deserialization logic. var o = _alternateParser(content); var r = JReader.FromAdapter(ReaderAdapters.FromSimpleTypes(o, allowTypeCoercion: true)); - return ParseJson(ref r); + return ParseJson(ref r, version); } } - private static FlagFileData ParseJson(string data) + private static FullDataSet ParseJson(string data, int version) { var r = JReader.FromString(data); - return ParseJson(ref r); + return ParseJson(ref r, version); } - private static FlagFileData ParseJson(ref JReader r) + private static FullDataSet ParseJson(ref JReader r, int version) { - var ret = new FlagFileData - { - Flags = new Dictionary(), - FlagValues = new Dictionary(), - Segments = new Dictionary() - }; + var flagsBuilder = ImmutableList.CreateBuilder>(); + var segmentsBuilder = ImmutableList.CreateBuilder>(); for (var obj = r.Object(); obj.Next(ref r);) { switch (obj.Name.ToString()) @@ -66,26 +64,72 @@ private static FlagFileData ParseJson(ref JReader r) case "flags": for (var subObj = r.ObjectOrNull(); subObj.Next(ref r);) { - ret.Flags[subObj.Name.ToString()] = FeatureFlagSerialization.Instance.ReadJson(ref r) as FeatureFlag; + var key = subObj.Name.ToString(); + var flag = FeatureFlagSerialization.Instance.ReadJson(ref r) as FeatureFlag; + flagsBuilder.Add(new KeyValuePair(key, new ItemDescriptor(version, + FlagWithVersion(flag, version)))); } break; case "flagValues": for (var subObj = r.ObjectOrNull(); subObj.Next(ref r);) { - ret.FlagValues[subObj.Name.ToString()] = LdValueConverter.ReadJsonValue(ref r); + var key = subObj.Name.ToString(); + var value = LdValueConverter.ReadJsonValue(ref r); + var flag = FlagWithValue(key, value, version); + flagsBuilder.Add(new KeyValuePair(key, new ItemDescriptor(version, flag))); } break; case "segments": for (var subObj = r.ObjectOrNull(); subObj.Next(ref r);) { - ret.Segments[subObj.Name.ToString()] = SegmentSerialization.Instance.ReadJson(ref r) as Segment; + var key = subObj.Name.ToString(); + var segment = SegmentSerialization.Instance.ReadJson(ref r) as Segment; + segmentsBuilder.Add(new KeyValuePair(key, new ItemDescriptor(version, + SegmentWithVersion(segment, version)))); } break; } } - return ret; + return new FullDataSet(ImmutableList.Create>>( + new KeyValuePair>(DataModel.Features, + new KeyedItems(flagsBuilder.ToImmutable())), + new KeyValuePair>(DataModel.Segments, + new KeyedItems(segmentsBuilder.ToImmutable())) + )); + } + + internal static FeatureFlag FlagWithVersion(FeatureFlag flag, int version) => + flag.Version == version ? flag : + new FeatureFlag( + flag.Key, + version, + flag.Deleted, flag.On, flag.Prerequisites, flag.Targets, flag.Rules, flag.Fallthrough, + flag.OffVariation, flag.Variations, flag.Salt, flag.TrackEvents, flag.TrackEventsFallthrough, + flag.DebugEventsUntilDate, flag.ClientSide); + + // Constructs a flag that always returns the same value. This is done by giving it a + // single variation and setting the fallthrough variation to that. + internal static object FlagWithValue(string key, LdValue value, int version) + { + var json = LdValue.BuildObject() + .Add("key", key) + .Add("version", version) + .Add("on", true) + .Add("variations", LdValue.ArrayOf(value)) + .Add("fallthrough", LdValue.BuildObject().Add("variation", 0).Build()) + .Build() + .ToJsonString(); + return DataModel.Features.Deserialize(json).Item; } + + internal static Segment SegmentWithVersion(Segment segment, int version) => + segment.Version == version ? segment : + new Segment( + segment.Key, + version, + segment.Deleted, segment.Included, segment.Excluded, segment.Rules, + segment.Salt, segment.Unbounded, segment.Generation); } } diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataSources/PollingProcessor.cs b/src/LaunchDarkly.ServerSdk/Internal/DataSources/PollingProcessor.cs index 9de4b53d..d01c191c 100644 --- a/src/LaunchDarkly.ServerSdk/Internal/DataSources/PollingProcessor.cs +++ b/src/LaunchDarkly.ServerSdk/Internal/DataSources/PollingProcessor.cs @@ -4,6 +4,7 @@ using LaunchDarkly.JsonStream; using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Internal; +using LaunchDarkly.Sdk.Internal.Concurrent; using LaunchDarkly.Sdk.Internal.Http; using LaunchDarkly.Sdk.Server.Interfaces; diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataSources/StreamProcessor.cs b/src/LaunchDarkly.ServerSdk/Internal/DataSources/StreamProcessor.cs index 61326891..e1da0285 100644 --- a/src/LaunchDarkly.ServerSdk/Internal/DataSources/StreamProcessor.cs +++ b/src/LaunchDarkly.ServerSdk/Internal/DataSources/StreamProcessor.cs @@ -6,6 +6,7 @@ using LaunchDarkly.JsonStream; using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Internal; +using LaunchDarkly.Sdk.Internal.Concurrent; using LaunchDarkly.Sdk.Internal.Events; using LaunchDarkly.Sdk.Internal.Http; using LaunchDarkly.Sdk.Server.Interfaces; diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataStores/DataStoreUpdatesImpl.cs b/src/LaunchDarkly.ServerSdk/Internal/DataStores/DataStoreUpdatesImpl.cs index 565daf1b..69a2ff9b 100644 --- a/src/LaunchDarkly.ServerSdk/Internal/DataStores/DataStoreUpdatesImpl.cs +++ b/src/LaunchDarkly.ServerSdk/Internal/DataStores/DataStoreUpdatesImpl.cs @@ -1,4 +1,7 @@ using System; +using LaunchDarkly.Logging; +using LaunchDarkly.Sdk.Internal; +using LaunchDarkly.Sdk.Internal.Concurrent; using LaunchDarkly.Sdk.Server.Interfaces; namespace LaunchDarkly.Sdk.Server.Internal.DataStores @@ -6,43 +9,33 @@ namespace LaunchDarkly.Sdk.Server.Internal.DataStores internal sealed class DataStoreUpdatesImpl : IDataStoreUpdates { private readonly TaskExecutor _taskExecutor; - private readonly object _stateLock = new object(); - private DataStoreStatus _currentStatus = new DataStoreStatus - { - Available = true, - RefreshNeeded = false - }; + private StateMonitor _status; - internal DataStoreStatus Status - { - get - { - lock(_stateLock) - { - return _currentStatus; - } - } - } + internal DataStoreStatus Status => _status.Current; internal event EventHandler StatusChanged; - internal DataStoreUpdatesImpl(TaskExecutor taskExecutor) + internal DataStoreUpdatesImpl(TaskExecutor taskExecutor, Logger log) { _taskExecutor = taskExecutor; + var initialStatus = new DataStoreStatus + { + Available = true, + RefreshNeeded = false + }; + _status = new StateMonitor(initialStatus, MaybeUpdate, log); } + private DataStoreStatus? MaybeUpdate(DataStoreStatus lastValue, DataStoreStatus newValue) => + newValue.Equals(lastValue) ? (DataStoreStatus?)null : newValue; + public void UpdateStatus(DataStoreStatus newStatus) { - lock (_stateLock) + if (_status.Update(newStatus, out _)) { - if (newStatus.Equals(_currentStatus)) - { - return; - } - _currentStatus = newStatus; + _taskExecutor.ScheduleEvent(newStatus, StatusChanged); } - _taskExecutor.ScheduleEvent(this, newStatus, StatusChanged); } } } diff --git a/src/LaunchDarkly.ServerSdk/Internal/DataStores/PersistentDataStoreStatusManager.cs b/src/LaunchDarkly.ServerSdk/Internal/DataStores/PersistentDataStoreStatusManager.cs index 3ada533c..b3150143 100644 --- a/src/LaunchDarkly.ServerSdk/Internal/DataStores/PersistentDataStoreStatusManager.cs +++ b/src/LaunchDarkly.ServerSdk/Internal/DataStores/PersistentDataStoreStatusManager.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Internal; +using LaunchDarkly.Sdk.Internal.Concurrent; using LaunchDarkly.Sdk.Server.Interfaces; namespace LaunchDarkly.Sdk.Server.Internal.DataStores diff --git a/src/LaunchDarkly.ServerSdk/Internal/TaskExecutor.cs b/src/LaunchDarkly.ServerSdk/Internal/TaskExecutor.cs deleted file mode 100644 index 679c804b..00000000 --- a/src/LaunchDarkly.ServerSdk/Internal/TaskExecutor.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using LaunchDarkly.Logging; -using LaunchDarkly.Sdk.Internal; - -namespace LaunchDarkly.Sdk.Server.Internal -{ - /// - /// Abstraction of scheduling infrequent worker tasks. - /// - /// - /// We use this instead of just calling Task.Run() for two reasons. First, the default - /// scheduling behavior of Task.Run() may not always be what we want. Second, this provides - /// better error logging. - /// - internal sealed class TaskExecutor - { - private readonly Logger _log; - - internal TaskExecutor(Logger log) - { - _log = log; - } - - /// - /// Schedules delivery of an event to some number of event handlers. - /// - /// - /// In the current implementation, each handler call is a separate background task. - /// - /// the event type - /// passed as the sender parameter to the handlers - /// the event object - /// a handler list - public void ScheduleEvent(object sender, T eventArgs, EventHandler handlers) - { - if (handlers is null) - { - return; - } - _log.Debug("scheduling task to send {0} to {1}", eventArgs, handlers); - foreach (var handler in handlers.GetInvocationList()) - { - _ = Task.Run(() => - { - _log.Debug("sending {0}", eventArgs); - try - { - handler.DynamicInvoke(sender, eventArgs); - } - catch (Exception e) - { - if (e is TargetInvocationException wrappedException) - { - e = wrappedException.InnerException; - } - LogHelpers.LogException(_log, "Unexpected exception from event handler", e); - } - }); - } - } - - /// - /// Starts a repeating async task. - /// - /// time to wait before first execution - /// interval at which to repeat - /// the task to run - /// a for stopping the task - public CancellationTokenSource StartRepeatingTask( - TimeSpan initialDelay, - TimeSpan interval, - Func taskFn - ) - { - var canceller = new CancellationTokenSource(); - _ = Task.Run(async () => - { - if (initialDelay.CompareTo(TimeSpan.Zero) > 0) - { - try - { - await Task.Delay(initialDelay, canceller.Token); - } - catch (TaskCanceledException) { } - } - while (true) - { - if (canceller.IsCancellationRequested) - { - return; - } - var nextTime = DateTime.Now.Add(interval); - try - { - await taskFn(); - } - catch (Exception e) - { - LogHelpers.LogException(_log, "Unexpected exception from repeating task", e); - } - var timeToWait = nextTime.Subtract(DateTime.Now); - if (timeToWait.CompareTo(TimeSpan.Zero) > 0) - { - try - { - await Task.Delay(timeToWait, canceller.Token); - } - catch (TaskCanceledException) { } - } - } - }); - return canceller; - } - } -} diff --git a/src/LaunchDarkly.ServerSdk/LaunchDarkly.ServerSdk.csproj b/src/LaunchDarkly.ServerSdk/LaunchDarkly.ServerSdk.csproj index 0ffa17cb..1e4c0d54 100644 --- a/src/LaunchDarkly.ServerSdk/LaunchDarkly.ServerSdk.csproj +++ b/src/LaunchDarkly.ServerSdk/LaunchDarkly.ServerSdk.csproj @@ -32,13 +32,16 @@ build products, which is necessary for our documentation generation logic; this doesn't affect what goes into the NuGet package. --> true + + + 1570,1571,1572,1573,1574,1580,1581,1584,1591,1710,1711,1712 - + diff --git a/src/LaunchDarkly.ServerSdk/LdClient.cs b/src/LaunchDarkly.ServerSdk/LdClient.cs index 3a969a53..1f295561 100644 --- a/src/LaunchDarkly.ServerSdk/LdClient.cs +++ b/src/LaunchDarkly.ServerSdk/LdClient.cs @@ -77,18 +77,18 @@ public sealed class LdClient : IDisposable, ILdClient /// constructor. The constructor returns as soon as any of the following things has happened: /// /// - /// It has successfully connected to LaunchDarkly and received feature flag data. In this + /// It has successfully connected to LaunchDarkly and received feature flag data. In this /// case, will be true, and the - /// will return a state of . - /// It has not succeeded in connecting within the + /// will return a state of . + /// It has not succeeded in connecting within the /// timeout (the default for this is 5 seconds). This could happen due to a network problem or a /// temporary service outage. In this case, will be false, and the /// will return a state of , - /// indicating that the SDK will still continue trying to connect in the background. - /// It has encountered an unrecoverable error: for instance, LaunchDarkly has rejected the + /// indicating that the SDK will still continue trying to connect in the background. + /// It has encountered an unrecoverable error: for instance, LaunchDarkly has rejected the /// SDK key. Since an invalid key will not become valid, the SDK will not retry in this case. /// will be false, and the will - /// return a state of . + /// return a state of . /// /// /// If you have specified mode or @@ -132,11 +132,11 @@ public LdClient(Configuration config) ServerDiagnosticStore diagnosticStore = _configuration.DiagnosticOptOut ? null : new ServerDiagnosticStore(_configuration, basicConfig, httpConfig); - var taskExecutor = new TaskExecutor(_log); + var taskExecutor = new TaskExecutor(this, _log); var clientContext = new LdClientContext(basicConfig, httpConfig, diagnosticStore, taskExecutor); - var dataStoreUpdates = new DataStoreUpdatesImpl(taskExecutor); + var dataStoreUpdates = new DataStoreUpdatesImpl(taskExecutor, _log.SubLogger(LogNames.DataStoreSubLog)); _dataStore = (_configuration.DataStoreFactory ?? Components.InMemoryDataStore) .CreateDataStore(clientContext, dataStoreUpdates); _dataStoreStatusProvider = new DataStoreStatusProviderImpl(_dataStore, dataStoreUpdates); diff --git a/test/LaunchDarkly.ServerSdk.Tests/AssertHelpers.cs b/test/LaunchDarkly.ServerSdk.Tests/AssertHelpers.cs index 1ce42157..f8412f87 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/AssertHelpers.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/AssertHelpers.cs @@ -1,208 +1,19 @@ -using System.Collections.Generic; -using System.Linq; -using Xunit; +using Xunit; using static LaunchDarkly.Sdk.Server.Interfaces.DataStoreTypes; +using static LaunchDarkly.TestHelpers.JsonAssertions; namespace LaunchDarkly.Sdk.Server { public static class AssertHelpers { - public static void FullyEqual(T a, T b) - { - Assert.Equal(a, b); - Assert.Equal(b, a); - Assert.Equal(a.GetHashCode(), b.GetHashCode()); - } - - public static void FullyUnequal(T a, T b) - { - Assert.NotEqual(a, b); - Assert.NotEqual(b, a); - } - - public static void JsonEqual(string expected, string actual) => - JsonEqual(LdValue.Parse(expected), LdValue.Parse(actual)); - - public static void JsonEqual(LdValue expected, LdValue actual) - { - if (!expected.Equals(actual)) - { - var diff = DescribeJsonDifference(expected, actual, ""); - if (diff is null) - { - Assert.Equal(expected, actual); // generates standard failure message - } - Assert.True(false, "JSON result mismatch:\n" + DescribeJsonDifference(expected, actual, "")); - } - } - - public static void DataSetsEqual(FullDataSet expected, FullDataSet actual) - { - var expectedNorm = TestUtils.NormalizeDataSet(expected); - var actualNorm = TestUtils.NormalizeDataSet(actual); - var ok = false; - string expectedDesc = null, actualDesc = null; - if (expectedNorm.Data.Count() == actualNorm.Data.Count()) - { - ok = true; - for (var i = 0; ok && i < expectedNorm.Data.Count(); i++) - { - var ec = expectedNorm.Data.ElementAt(i); - var ac = actualNorm.Data.ElementAt(i); - if (ac.Key != ec.Key) - { - ok = false; - } - else - { - var kind = ac.Key; - for (var j = 0; ok && j < ec.Value.Items.Count(); j++) - { - var ei = ec.Value.Items.ElementAt(j); - var ai = ac.Value.Items.ElementAt(j); - if (ai.Key != ei.Key || !ItemsEqual(kind, ei.Value, ai.Value)) - { - expectedDesc = DescribeDataCollection(kind, ei); - actualDesc = DescribeDataCollection(kind, ai); - ok = false; - } - } - } - } - } - if (!ok) - { - if (expectedDesc is null) - { - expectedDesc = DescribeDataSet(expectedNorm); - actualDesc = DescribeDataSet(actualNorm); - } - Assert.True(false, string.Format("data set mismatch:\nexpected: {0}\nactual: {1}", - expectedDesc, actualDesc)); - } - } + public static void DataSetsEqual(FullDataSet expected, FullDataSet actual) => + AssertJsonEqual(TestUtils.DataSetAsJson(expected), TestUtils.DataSetAsJson(actual)); public static void DataItemsEqual(DataKind kind, ItemDescriptor expected, ItemDescriptor actual) { - if (!ItemsEqual(kind, expected, actual)) - { - Assert.True(false, string.Format("expected: {0}\nactual: {1}", - kind.Serialize(expected), kind.Serialize(actual))); - } - } - - private static string DescribeJsonDifference(LdValue expected, LdValue actual, string prefix) - { - if (expected.Type == LdValueType.Object && actual.Type == LdValueType.Object) - { - return DescribeJsonObjectDifference(expected, actual, prefix); - } - if (expected.Type == LdValueType.Array && actual.Type == LdValueType.Array) - { - return DescribeJsonArrayDifference(expected, actual, prefix); - } - return null; + AssertJsonEqual(kind.Serialize(expected), kind.Serialize(actual)); + Assert.Equal(expected.Version, actual.Version); } - - private static string DescribeJsonObjectDifference(LdValue expected, LdValue actual, string prefix) - { - var expectedDict = expected.AsDictionary(LdValue.Convert.Json); - var actualDict = actual.AsDictionary(LdValue.Convert.Json); - var allKeys = expectedDict.Keys.Union(actualDict.Keys); - var lines = new List(); - foreach (var key in allKeys) - { - var prefixedKey = prefix + (prefix == "" ? "" : ".") + key; - string expectedDesc = null, actualDesc = null, detailDiff = null; - if (expectedDict.ContainsKey(key)) - { - if (actualDict.ContainsKey(key)) - { - LdValue expectedProp = expectedDict[key], actualProp = actualDict[key]; - if (expectedProp != actualProp) - { - expectedDesc = expectedProp.ToJsonString(); - actualDesc = actualProp.ToJsonString(); - detailDiff = DescribeJsonDifference(expectedProp, actualProp, prefixedKey); - } - } - else - { - expectedDesc = expectedDict[key].ToJsonString(); - actualDesc = ""; - } - } - else - { - actualDesc = actualDict[key].ToJsonString(); - expectedDesc = ""; - } - if (expectedDesc != null || actualDesc != null) - { - if (detailDiff != null) - { - lines.Add(detailDiff); - } - else - { - lines.Add(string.Format("property \"{0}\": expected = {1}, actual = {2}", - prefixedKey, expectedDesc, actualDesc)); - } - } - } - return string.Join("\n", lines); - } - - private static string DescribeJsonArrayDifference(LdValue expected, LdValue actual, string prefix) - { - if (expected.Count != actual.Count) - { - return null; // can't provide a detailed diff, just show the whole values - } - var lines = new List(); - for (var i = 0; i < expected.Count; i++) - { - var prefixedIndex = string.Format("{0}[{1}]", prefix, i); - LdValue expectedElement = expected.Get(i), actualElement = actual.Get(i); - if (actualElement != expectedElement) - { - var detailDiff = DescribeJsonDifference(expectedElement, actualElement, prefixedIndex); - if (detailDiff != null) - { - lines.Add(detailDiff); - } - else - { - lines.Add(string.Format("property \"{0}\": expected = {1}, actual = {2}", - prefixedIndex, expectedElement, actualElement)); - } - } - } - return string.Join("\n", lines); - } - - private static string DescribeDataSet(FullDataSet allData) => - string.Join(", ", allData.Data.Select(coll => - DescribeDataCollection(coll.Key, coll.Value.Items.ToArray()))); - - private static string DescribeDataCollection(DataKind kind, params KeyValuePair[] items) => - kind.Name.ToUpper() + ": [" + - string.Join(",", items.Select(keyedItem => - "[" + keyedItem.Key + ": " + kind.Serialize(keyedItem.Value))) - + "]"; - - private class KeyedItemEqualityComparer : IEqualityComparer> - { - public DataKind Kind { get; set; } - - public bool Equals(KeyValuePair x, KeyValuePair y) => - x.Key == y.Key && ItemsEqual(Kind, x.Value, y.Value); - - public int GetHashCode(KeyValuePair obj) => 0; - } - - private static bool ItemsEqual(DataKind kind, ItemDescriptor expected, ItemDescriptor actual) => - LdValue.Parse(kind.Serialize(actual)) == LdValue.Parse(kind.Serialize(expected)); } } diff --git a/test/LaunchDarkly.ServerSdk.Tests/BuilderTestUtil.cs b/test/LaunchDarkly.ServerSdk.Tests/BuilderTestUtil.cs deleted file mode 100644 index 3f084907..00000000 --- a/test/LaunchDarkly.ServerSdk.Tests/BuilderTestUtil.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using Xunit; - -namespace LaunchDarkly.Sdk.Server -{ - public static class BuilderTestUtil - { - // Use this when we want to test the effect of builder changes on the object that - // is eventually built. - public static BuilderTestUtil For( - Func constructor, Func buildMethod) => - new BuilderTestUtil(constructor, buildMethod, null); - - // Use this when we want to test the builder's internal state directly, without - // calling Build - i.e. if the object is difficult to inspect after it's built. - public static BuilderInternalTestUtil For(Func constructor) => - new BuilderInternalTestUtil(constructor); - } - - public class BuilderTestUtil - { - private readonly Func _constructor; - internal readonly Func _buildMethod; - internal readonly Func _copyConstructor; - - public BuilderTestUtil(Func constructor, - Func buildMethod, - Func copyConstructor - ) - { - _constructor = constructor; - _buildMethod = buildMethod; - _copyConstructor = copyConstructor; - } - - public BuilderPropertyTestUtil Property( - Func getter, - Action builderSetter - ) => - new BuilderPropertyTestUtil( - this, getter, builderSetter); - - public TBuilder New() => _constructor(); - - public BuilderTestUtil WithCopyConstructor( - Func copyConstructor - ) => - new BuilderTestUtil(_constructor, _buildMethod, copyConstructor); - } - - public class BuilderPropertyTestUtil - { - private readonly BuilderTestUtil _owner; - private readonly Func _getter; - private readonly Action _builderSetter; - - public BuilderPropertyTestUtil(BuilderTestUtil owner, - Func getter, - Action builderSetter) - { - _owner = owner; - _getter = getter; - _builderSetter = builderSetter; - } - - public void AssertDefault(TValue defaultValue) - { - var b = _owner.New(); - AssertValue(b, defaultValue); - } - - public void AssertCanSet(TValue newValue) - { - AssertSetIsChangedTo(newValue, newValue); - } - - public void AssertSetIsChangedTo(TValue attemptedValue, TValue resultingValue) - { - var b = _owner.New(); - _builderSetter(b, attemptedValue); - AssertValue(b, resultingValue); - } - - private void AssertValue(TBuilder b, TValue v) - { - var o = _owner._buildMethod(b); - Assert.Equal(v, _getter(o)); - if (_owner._copyConstructor != null) - { - var b1 = _owner._copyConstructor(o); - var o1 = _owner._buildMethod(b); - Assert.Equal(v, _getter(o)); - } - } - } - - public class BuilderInternalTestUtil - { - private readonly Func _constructor; - - public BuilderInternalTestUtil(Func constructor) - { - _constructor = constructor; - } - - public BuilderInternalPropertyTestUtil Property( - Func builderGetter, - Action builderSetter - ) => - new BuilderInternalPropertyTestUtil(this, - builderGetter, builderSetter); - - public TBuilder New() => _constructor(); - } - - public class BuilderInternalPropertyTestUtil - { - private readonly BuilderInternalTestUtil _owner; - private readonly Func _builderGetter; - private readonly Action _builderSetter; - - public BuilderInternalPropertyTestUtil(BuilderInternalTestUtil owner, - Func builderGetter, - Action builderSetter) - { - _owner = owner; - _builderGetter = builderGetter; - _builderSetter = builderSetter; - } - - public void AssertDefault(TValue defaultValue) - { - Assert.Equal(defaultValue, _builderGetter(_owner.New())); - } - - public void AssertCanSet(TValue newValue) - { - AssertSetIsChangedTo(newValue, newValue); - } - - public void AssertSetIsChangedTo(TValue attemptedValue, TValue resultingValue) - { - var b = _owner.New(); - _builderSetter(b, attemptedValue); - Assert.Equal(resultingValue, _builderGetter(b)); - } - } -} diff --git a/test/LaunchDarkly.ServerSdk.Tests/ConfigurationTest.cs b/test/LaunchDarkly.ServerSdk.Tests/ConfigurationTest.cs index 0baf92dd..52ead506 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/ConfigurationTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/ConfigurationTest.cs @@ -2,14 +2,15 @@ using LaunchDarkly.Logging; using LaunchDarkly.Sdk.Server.Internal; using LaunchDarkly.Sdk.Server.Internal.DataStores; +using LaunchDarkly.TestHelpers; using Xunit; namespace LaunchDarkly.Sdk.Server { public class ConfigurationTest { - private readonly BuilderTestUtil _tester = - BuilderTestUtil.For(() => Configuration.Builder(sdkKey), b => b.Build()) + private readonly BuilderBehavior.BuildTester _tester = + BuilderBehavior.For(() => Configuration.Builder(sdkKey), b => b.Build()) .WithCopyConstructor(c => Configuration.Builder(c)); const string sdkKey = "any-key"; diff --git a/test/LaunchDarkly.ServerSdk.Tests/DataModelTest.cs b/test/LaunchDarkly.ServerSdk.Tests/DataModelTest.cs index 0f599a39..3f7e68d1 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/DataModelTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/DataModelTest.cs @@ -4,6 +4,7 @@ using Xunit; using static LaunchDarkly.Sdk.Server.Interfaces.DataStoreTypes; +using static LaunchDarkly.TestHelpers.JsonAssertions; namespace LaunchDarkly.Sdk.Server { @@ -23,20 +24,20 @@ public void DataKindNames() public void SerializeAndDeserializeFlag() { var expectedJson = FlagWithAllPropertiesJson(); - var flag = MustParseFlag(expectedJson.ToJsonString()); + var flag = MustParseFlag(expectedJson); AssertFlagHasAllProperties(flag); var s = DataModel.Features.Serialize(new ItemDescriptor(flag.Version, flag)); - AssertHelpers.JsonEqual(expectedJson, LdValue.Parse(s)); + AssertJsonEqual(expectedJson, s); } [Fact] public void SerializeAndDeserializeSegment() { var expectedJson = SegmentWithAllPropertiesJson(); - var segment = MustParseSegment(expectedJson.ToJsonString()); + var segment = MustParseSegment(expectedJson); AssertSegmentHasAllProperties(segment); var s = DataModel.Segments.Serialize(new ItemDescriptor(segment.Version, segment)); - AssertHelpers.JsonEqual(expectedJson, LdValue.Parse(s)); + AssertJsonEqual(expectedJson, s); } [Fact] @@ -46,13 +47,13 @@ public void SerializeDeletedItems() // of our existing database integrations aren't able to store the version number separately from // the JSON data. var deletedItem = ItemDescriptor.Deleted(2); - var expected = LdValue.BuildObject().Add("version", 2).Add("deleted", true).Build(); + var expected = LdValue.BuildObject().Add("version", 2).Add("deleted", true).Build().ToJsonString(); var s1 = DataModel.Features.Serialize(deletedItem); - AssertHelpers.JsonEqual(expected, LdValue.Parse(s1)); + AssertJsonEqual(expected, s1); var s2 = DataModel.Segments.Serialize(deletedItem); - AssertHelpers.JsonEqual(expected, LdValue.Parse(s2)); + AssertJsonEqual(expected, s2); } [Fact] @@ -166,9 +167,7 @@ private Segment MustParseSegment(string json) return segment; } - private LdValue FlagWithAllPropertiesJson() - { - return LdValue.Parse(@"{ + private string FlagWithAllPropertiesJson() => @"{ ""key"": ""flag-key"", ""version"": 99, ""deleted"": false, @@ -216,8 +215,7 @@ private LdValue FlagWithAllPropertiesJson() ""trackEvents"": true, ""trackEventsFallthrough"": true, ""debugEventsUntilDate"": 1000 -}"); - } +}"; private void AssertFlagHasAllProperties(FeatureFlag flag) { @@ -278,10 +276,9 @@ private void AssertFlagHasAllProperties(FeatureFlag flag) Assert.Equal(UserAttribute.Email, r.Rollout.Value.BucketBy); Assert.Equal(RolloutKind.Experiment, r.Rollout.Value.Kind); Assert.Equal(123, r.Rollout.Value.Seed); - Assert.Collection(r.Clauses); + Assert.Empty(r.Clauses); }); - Assert.NotNull(flag.Fallthrough); Assert.Equal(1, flag.Fallthrough.Variation); Assert.Null(flag.Fallthrough.Rollout); Assert.Equal(2, flag.OffVariation); @@ -292,9 +289,7 @@ private void AssertFlagHasAllProperties(FeatureFlag flag) Assert.Equal(UnixMillisecondTime.OfMillis(1000), flag.DebugEventsUntilDate); } - private LdValue SegmentWithAllPropertiesJson() - { - return LdValue.Parse(@"{ + private string SegmentWithAllPropertiesJson() => @"{ ""key"": ""segment-key"", ""version"": 99, ""deleted"": false, @@ -320,8 +315,7 @@ private LdValue SegmentWithAllPropertiesJson() ], ""unbounded"": true, ""generation"": 51 -}"); - } +}"; private void AssertSegmentHasAllProperties(Segment segment) { @@ -349,7 +343,7 @@ private void AssertSegmentHasAllProperties(Segment segment) { Assert.Null(r.Weight); Assert.Null(r.BucketBy); - Assert.Collection(r.Clauses); + Assert.Empty(r.Clauses); }); Assert.True(segment.Unbounded); diff --git a/test/LaunchDarkly.ServerSdk.Tests/FeatureFlagsStateTest.cs b/test/LaunchDarkly.ServerSdk.Tests/FeatureFlagsStateTest.cs index 15ccf140..1dccd620 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/FeatureFlagsStateTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/FeatureFlagsStateTest.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using LaunchDarkly.Sdk.Json; +using LaunchDarkly.TestHelpers; using Xunit; namespace LaunchDarkly.Sdk.Server @@ -85,7 +86,7 @@ public void CanSerializeToJson() ""$valid"":true }"; var actualString = LdJsonSerialization.SerializeObject(state); - AssertHelpers.JsonEqual(expectedString, actualString); + JsonAssertions.AssertJsonEqual(expectedString, actualString); } [Fact] diff --git a/test/LaunchDarkly.ServerSdk.Tests/Integrations/BigSegmentsConfigurationBuilderTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Integrations/BigSegmentsConfigurationBuilderTest.cs index bd85e631..69a29ce8 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Integrations/BigSegmentsConfigurationBuilderTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Integrations/BigSegmentsConfigurationBuilderTest.cs @@ -1,5 +1,6 @@ using System; using LaunchDarkly.Sdk.Server.Interfaces; +using LaunchDarkly.TestHelpers; using Moq; using Xunit; using Xunit.Abstractions; @@ -10,7 +11,7 @@ public class BigSegmentsConfigurationBuilderTest : BaseTest { private readonly IBigSegmentStore _store; private readonly IBigSegmentStoreFactory _storeFactory; - private readonly BuilderInternalTestUtil _tester; + private readonly BuilderBehavior.InternalStateTester _tester; public BigSegmentsConfigurationBuilderTest(ITestOutputHelper testOutput) : base(testOutput) { @@ -20,7 +21,7 @@ public BigSegmentsConfigurationBuilderTest(ITestOutputHelper testOutput) : base( _storeFactory = storeFactoryMock.Object; storeFactoryMock.Setup(f => f.CreateBigSegmentStore(basicContext)).Returns(_store); - _tester = BuilderTestUtil.For(() => Components.BigSegments(_storeFactory)); + _tester = BuilderBehavior.For(() => Components.BigSegments(_storeFactory)); } [Fact] diff --git a/test/LaunchDarkly.ServerSdk.Tests/Integrations/EventProcessorBuilderTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Integrations/EventProcessorBuilderTest.cs index 43199c06..662b7788 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Integrations/EventProcessorBuilderTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Integrations/EventProcessorBuilderTest.cs @@ -1,13 +1,14 @@ using System; using System.Collections.Generic; +using LaunchDarkly.TestHelpers; using Xunit; namespace LaunchDarkly.Sdk.Server.Integrations { public class EventProcessorBuilderTest { - private readonly BuilderInternalTestUtil _tester = - BuilderTestUtil.For(Components.SendEvents); + private readonly BuilderBehavior.InternalStateTester _tester = + BuilderBehavior.For(Components.SendEvents); [Fact] public void AllAttributesPrivate() diff --git a/test/LaunchDarkly.ServerSdk.Tests/Integrations/FileDataSourceBuilderTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Integrations/FileDataSourceBuilderTest.cs index 9d1fad05..aa1ed3a5 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Integrations/FileDataSourceBuilderTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Integrations/FileDataSourceBuilderTest.cs @@ -1,14 +1,15 @@ using System; using System.Collections.Generic; using LaunchDarkly.Sdk.Server.Internal.DataSources; +using LaunchDarkly.TestHelpers; using Xunit; namespace LaunchDarkly.Sdk.Server.Integrations { public class FileDataSourceBuilderTest { - private readonly BuilderInternalTestUtil _tester = - BuilderTestUtil.For(FileData.DataSource); + private readonly BuilderBehavior.InternalStateTester _tester = + BuilderBehavior.For(FileData.DataSource); [Fact] public void AutoUpdate() diff --git a/test/LaunchDarkly.ServerSdk.Tests/Integrations/HttpConfigurationBuilderTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Integrations/HttpConfigurationBuilderTest.cs index 9f04cf19..472f3403 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Integrations/HttpConfigurationBuilderTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Integrations/HttpConfigurationBuilderTest.cs @@ -4,6 +4,7 @@ using System.Net.Http; using LaunchDarkly.Sdk.Internal; using LaunchDarkly.Sdk.Server.Interfaces; +using LaunchDarkly.TestHelpers; using Xunit; namespace LaunchDarkly.Sdk.Server.Integrations @@ -13,8 +14,8 @@ public class HttpConfigurationBuilderTest private static readonly BasicConfiguration basicConfig = new BasicConfiguration("sdk-key", false, null); - private readonly BuilderTestUtil _tester = - BuilderTestUtil.For(() => Components.HttpConfiguration(), + private readonly BuilderBehavior.BuildTester _tester = + BuilderBehavior.For(() => Components.HttpConfiguration(), b => b.CreateHttpConfiguration(basicConfig)); [Fact] diff --git a/test/LaunchDarkly.ServerSdk.Tests/Integrations/PollingDataSourceBuilderTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Integrations/PollingDataSourceBuilderTest.cs index 07419a0b..96f928b8 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Integrations/PollingDataSourceBuilderTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Integrations/PollingDataSourceBuilderTest.cs @@ -1,12 +1,13 @@ using System; +using LaunchDarkly.TestHelpers; using Xunit; namespace LaunchDarkly.Sdk.Server.Integrations { public class PollingDataSourceBuilderTest { - private readonly BuilderInternalTestUtil _tester = - BuilderTestUtil.For(Components.PollingDataSource); + private readonly BuilderBehavior.InternalStateTester _tester = + BuilderBehavior.For(Components.PollingDataSource); [Fact] public void BaseUri() diff --git a/test/LaunchDarkly.ServerSdk.Tests/Integrations/StreamingDataSourceBuilderTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Integrations/StreamingDataSourceBuilderTest.cs index b55ac89e..f4fd0d16 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Integrations/StreamingDataSourceBuilderTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Integrations/StreamingDataSourceBuilderTest.cs @@ -1,12 +1,13 @@ using System; +using LaunchDarkly.TestHelpers; using Xunit; namespace LaunchDarkly.Sdk.Server.Integrations { public class StreamingDataSourceBuilderTest { - private readonly BuilderInternalTestUtil _tester = - BuilderTestUtil.For(Components.StreamingDataSource); + private readonly BuilderBehavior.InternalStateTester _tester = + BuilderBehavior.For(Components.StreamingDataSource); [Fact] public void BaseUri() diff --git a/test/LaunchDarkly.ServerSdk.Tests/Integrations/TestDataTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Integrations/TestDataTest.cs index 699c711d..2b90647f 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Integrations/TestDataTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Integrations/TestDataTest.cs @@ -5,6 +5,7 @@ using Xunit.Abstractions; using static LaunchDarkly.Sdk.Server.Interfaces.DataStoreTypes; +using static LaunchDarkly.TestHelpers.JsonAssertions; namespace LaunchDarkly.Sdk.Server.Integrations { @@ -30,18 +31,18 @@ public void InitializesWithEmptyData() { CreateAndStart(); - Assert.Equal(1, _updates.Inits.Count); + Assert.Single(_updates.Inits); var data = TestUtils.NormalizeDataSet(_updates.Inits.Take()); Assert.Collection(data.Data, coll => { Assert.Equal(DataModel.Features, coll.Key); - Assert.Collection(coll.Value.Items); + Assert.Empty(coll.Value.Items); }, coll => { Assert.Equal(DataModel.Segments, coll.Key); - Assert.Collection(coll.Value.Items); + Assert.Empty(coll.Value.Items); }); } @@ -53,7 +54,7 @@ public void InitializesWithFlags() CreateAndStart(); - Assert.Equal(1, _updates.Inits.Count); + Assert.Single(_updates.Inits); var data = TestUtils.NormalizeDataSet(_updates.Inits.Take()); Assert.Collection(data.Data, coll => @@ -69,7 +70,7 @@ public void InitializesWithFlags() coll => { Assert.Equal(DataModel.Segments, coll.Key); - Assert.Collection(coll.Value.Items); + Assert.Empty(coll.Value.Items); }); } @@ -80,7 +81,7 @@ public void AddsFlag() _td.Update(_td.Flag("flag1").On(true)); - Assert.Equal(1, _updates.Upserts.Count); + Assert.Single(_updates.Upserts); var up = _updates.Upserts.Take(); Assert.Equal(DataModel.Features, up.Kind); AssertFlag("flag1", 1, up.Key, up.Item, json => @@ -93,11 +94,11 @@ public void UpdatesFlag() _td.Update(_td.Flag("flag1").On(true)); CreateAndStart(); - Assert.Equal(0, _updates.Upserts.Count); + Assert.Empty(_updates.Upserts); _td.Update(_td.Flag("flag1").On(true)); - Assert.Equal(1, _updates.Upserts.Count); + Assert.Single(_updates.Upserts); var up = _updates.Upserts.Take(); Assert.Equal(DataModel.Features, up.Kind); AssertFlag("flag1", 2, up.Key, up.Item, json => @@ -303,10 +304,9 @@ private void VerifyFlag(Func configu td.Update(configureFlag(_td.Flag("flagkey"))); - Assert.Equal(1, _updates.Upserts.Count); + Assert.Single(_updates.Upserts); var up = _updates.Upserts.Take(); - var json = LdValue.Parse(DataModel.Features.Serialize(up.Item)); - AssertHelpers.JsonEqual(LdValue.Parse(expectedJson), json); + AssertJsonEqual(expectedJson, DataModel.Features.Serialize(up.Item)); } } } diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/BigSegmentsStatusProviderImplTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/BigSegmentsStatusProviderImplTest.cs index 980e5876..e92aad5a 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/BigSegmentsStatusProviderImplTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/BigSegmentsStatusProviderImplTest.cs @@ -1,4 +1,5 @@ using System; +using LaunchDarkly.Sdk.Internal; using LaunchDarkly.Sdk.Server.Interfaces; using Moq; using Xunit; @@ -43,7 +44,7 @@ public void StatusProviderDelegatesToStoreWrapper() .StaleAfter(TimeSpan.FromDays(1)); using (var sw = new BigSegmentStoreWrapper( bsConfig.CreateBigSegmentsConfiguration(basicContext), - new TaskExecutor(testLogger), + new TaskExecutor(null, testLogger), testLogger )) { diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/BigSegmentsStoreWrapperTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/BigSegmentsStoreWrapperTest.cs index da12d710..fe98430b 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/BigSegmentsStoreWrapperTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/BigSegmentsStoreWrapperTest.cs @@ -1,5 +1,7 @@ using System; +using LaunchDarkly.Sdk.Internal; using LaunchDarkly.Sdk.Server.Interfaces; +using LaunchDarkly.TestHelpers; using Moq; using Xunit; using Xunit.Abstractions; @@ -24,7 +26,7 @@ public BigSegmentsStoreWrapperTest(ITestOutputHelper testOutput) : base(testOutp _storeFactory = storeFactoryMock.Object; storeFactoryMock.Setup(f => f.CreateBigSegmentStore(basicContext)).Returns(_store); - _taskExecutor = new TaskExecutor(testLogger); + _taskExecutor = new TaskExecutor(this, testLogger); } private void SetStoreHasNoMetadata() => diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/MembershipBuilderTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/MembershipBuilderTest.cs index 7c6cf8ff..3f986417 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/MembershipBuilderTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/BigSegments/MembershipBuilderTest.cs @@ -1,4 +1,5 @@ -using Xunit; +using LaunchDarkly.TestHelpers; +using Xunit; using static LaunchDarkly.Sdk.Server.Interfaces.BigSegmentStoreTypes; @@ -19,7 +20,7 @@ public void EmptyMembership() Assert.Same(m0, m1); Assert.Same(m0, m2); - AssertHelpers.FullyEqual(m0, m1); + TypeBehavior.AssertEqual(m0, m1); Assert.Null(m0.CheckMembership("arbitrary")); } @@ -31,14 +32,14 @@ public void MembershipWithSingleIncludeOnly() var m1 = NewMembershipFromSegmentRefs(new string[] { "key1" }, null); Assert.NotSame(m0, m1); - AssertHelpers.FullyEqual(m0, m1); + TypeBehavior.AssertEqual(m0, m1); Assert.True(m0.CheckMembership("key1")); Assert.Null(m0.CheckMembership("key2")); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(null, new string[] { "key1" })); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(new string[] { "key2" }, null)); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(null, null)); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(null, new string[] { "key1" })); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(new string[] { "key2" }, null)); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(null, null)); } [Fact] @@ -48,18 +49,18 @@ public void MembershipWithMultipleIncludesOnly() var m1 = NewMembershipFromSegmentRefs(new string[] { "key2", "key1" }, null); Assert.NotSame(m0, m1); - AssertHelpers.FullyEqual(m0, m1); + TypeBehavior.AssertEqual(m0, m1); Assert.True(m0.CheckMembership("key1")); Assert.True(m0.CheckMembership("key2")); Assert.Null(m0.CheckMembership("key3")); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs( + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs( new string[] { "key1", "key2" }, new string[] { "key3" })); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs( + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs( new string[] { "key1", "key3" }, null)); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(new string[] { "key1" }, null)); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(null, null)); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(new string[] { "key1" }, null)); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(null, null)); } [Fact] @@ -69,14 +70,14 @@ public void MembershipWithSingleExcludeOnly() var m1 = NewMembershipFromSegmentRefs(null, new string[] { "key1" }); Assert.NotSame(m0, m1); - AssertHelpers.FullyEqual(m0, m1); + TypeBehavior.AssertEqual(m0, m1); Assert.False(m0.CheckMembership("key1")); Assert.Null(m0.CheckMembership("key2")); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(new string[] { "key1" }, null)); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(null, new string[] { "key2" })); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(null, null)); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(new string[] { "key1" }, null)); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(null, new string[] { "key2" })); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(null, null)); } [Fact] @@ -86,18 +87,18 @@ public void MembershipWithMultipleExcludesOnly() var m1 = NewMembershipFromSegmentRefs(null, new string[] { "key2", "key1" }); Assert.NotSame(m0, m1); - AssertHelpers.FullyEqual(m0, m1); + TypeBehavior.AssertEqual(m0, m1); Assert.False(m0.CheckMembership("key1")); Assert.False(m0.CheckMembership("key2")); Assert.Null(m0.CheckMembership("key3")); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs( + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs( new string[] { "key3" }, new string[] { "key1", "key2" })); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs( + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs( null, new string[] { "key1", "key3" })); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(null, new string[] { "key1" })); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(null, null)); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(null, new string[] { "key1" })); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(null, null)); } [Fact] @@ -114,7 +115,7 @@ public void MembershipWithIncludesAndExcludes() new string[] { "key3", "key2" } ); Assert.NotSame(m0, m1); - AssertHelpers.FullyEqual(m0, m0); + TypeBehavior.AssertEqual(m0, m0); Assert.True(m0.CheckMembership("key1")); Assert.True(m0.CheckMembership("key2")); @@ -122,12 +123,12 @@ public void MembershipWithIncludesAndExcludes() Assert.Null(m0.CheckMembership("key4")); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs( + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs( new string[] { "key1", "key2" }, new string[] { "key2", "key3", "key4" })); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs( + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs( new string[] { "key1", "key2", "key3" }, new string[] { "key2", "key3" })); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(new string[] { "key1" }, null)); - AssertHelpers.FullyUnequal(m0, NewMembershipFromSegmentRefs(null, null)); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(new string[] { "key1" }, null)); + TypeBehavior.AssertNotEqual(m0, NewMembershipFromSegmentRefs(null, null)); } } } diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/DataSourceStatusProviderImplTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/DataSourceStatusProviderImplTest.cs index aac74237..b9298881 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/DataSourceStatusProviderImplTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/DataSourceStatusProviderImplTest.cs @@ -2,6 +2,7 @@ using System.Threading; using LaunchDarkly.Sdk.Server.Interfaces; using LaunchDarkly.Sdk.Server.Internal.DataStores; +using LaunchDarkly.TestHelpers; using Xunit; using Xunit.Abstractions; diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/DataSourceUpdatesImplTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/DataSourceUpdatesImplTest.cs index fca5eaf6..b0c75b36 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/DataSourceUpdatesImplTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/DataSourceUpdatesImplTest.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using LaunchDarkly.Logging; +using LaunchDarkly.Sdk.Internal; using LaunchDarkly.Sdk.Server.Interfaces; using LaunchDarkly.Sdk.Server.Internal.DataStores; using LaunchDarkly.Sdk.Server.Internal.Model; +using LaunchDarkly.TestHelpers; using Xunit; using Xunit.Abstractions; @@ -41,9 +44,9 @@ private DataSourceUpdatesImpl MakeInstance() => public DataSourceUpdatesImplTest(ITestOutputHelper testOutput) : base(testOutput) { - taskExecutor = new TaskExecutor(testLogger); + taskExecutor = new TaskExecutor(this, testLogger); store = new InMemoryDataStore(); - dataStoreUpdates = new DataStoreUpdatesImpl(taskExecutor); + dataStoreUpdates = new DataStoreUpdatesImpl(taskExecutor, testLogger); dataStoreStatusProvider = new DataStoreStatusProviderImpl(store, dataStoreUpdates); } @@ -370,12 +373,11 @@ public void OutageTimeoutLogging() DateTime deadline = DateTime.Now.AddSeconds(1); while (DateTime.Now < deadline) { - var messages = logCapture.GetMessages(); + var messages = logCapture.GetMessages().Where(m => m.Level == LogLevel.Error).ToList(); if (messages.Count == 1) { var m = messages[0]; - if (m.Level == LogLevel.Error && - m.LoggerName == ".DataSource" && + if (m.LoggerName == ".DataSource" && m.Text.Contains("NETWORK_ERROR (1 time)") && m.Text.Contains("ERROR_RESPONSE(501) (2 times)") && m.Text.Contains("ERROR_RESPONSE(502) (1 time)")) diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/FileDataSourceTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/FileDataSourceTest.cs index 28929c2a..fe9ec811 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/FileDataSourceTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/FileDataSourceTest.cs @@ -1,13 +1,16 @@ using System; -using System.Linq; -using System.Threading; using LaunchDarkly.Sdk.Server.Integrations; using LaunchDarkly.Sdk.Server.Interfaces; -using LaunchDarkly.Sdk.Server.Internal.DataStores; +using LaunchDarkly.Sdk.Server.Internal.Model; +using LaunchDarkly.TestHelpers; using YamlDotNet.Serialization; using Xunit; using Xunit.Abstractions; +using static LaunchDarkly.Sdk.Server.Interfaces.DataStoreTypes; +using static LaunchDarkly.Sdk.Server.TestUtils; +using static LaunchDarkly.TestHelpers.JsonAssertions; + namespace LaunchDarkly.Sdk.Server.Internal.DataSources { public class FileDataSourceTest : BaseTest @@ -16,7 +19,7 @@ public class FileDataSourceTest : BaseTest private static readonly string ALL_DATA_JSON_FILE = TestUtils.TestFilePath("all-properties.json"); private static readonly string ALL_DATA_YAML_FILE = TestUtils.TestFilePath("all-properties.yml"); - private readonly IDataStore store = new InMemoryDataStore(); + private readonly CapturingDataSourceUpdates _updateSink = new CapturingDataSourceUpdates(); private readonly FileDataSourceBuilder factory = FileData.DataSource(); private readonly User user = User.WithKey("key"); @@ -25,13 +28,13 @@ public FileDataSourceTest(ITestOutputHelper testOutput) : base(testOutput) { } private Configuration MakeConfig() => Configuration.Builder(sdkKey) .Events(Components.NoEvents) - .Logging(Components.Logging(testLogging)) + .Logging(testLogging) .Build(); private IDataSource MakeDataSource() => factory.CreateDataSource( new LdClientContext(new BasicConfiguration(sdkKey, false, testLogger), MakeConfig()), - TestUtils.BasicDataSourceUpdates(store, testLogger)); + _updateSink); [Fact] public void FlagsAreNotLoadedUntilStart() @@ -39,9 +42,7 @@ public void FlagsAreNotLoadedUntilStart() factory.FilePaths(ALL_DATA_JSON_FILE); using (var fp = MakeDataSource()) { - Assert.False(store.Initialized()); - Assert.Equal(0, CountFlagsInStore()); - Assert.Equal(0, CountSegmentsInStore()); + Assert.False(_updateSink.Inits.TryTake(out _, TimeSpan.FromMilliseconds(100))); } } @@ -52,9 +53,8 @@ public void FlagsAreLoadedOnStart() using (var fp = MakeDataSource()) { fp.Start(); - Assert.True(store.Initialized()); - Assert.Equal(2, CountFlagsInStore()); - Assert.Equal(1, CountSegmentsInStore()); + Assert.True(_updateSink.Inits.TryTake(out var initData, TimeSpan.FromMilliseconds(100))); + AssertJsonEqual(DataSetAsJson(ExpectedDataSetForFullDataFile(1)), DataSetAsJson(initData)); } } @@ -67,9 +67,8 @@ public void FlagsCanBeLoadedWithExternalYamlParser() using (var fp = MakeDataSource()) { fp.Start(); - Assert.True(store.Initialized()); - Assert.Equal(2, CountFlagsInStore()); - Assert.Equal(1, CountSegmentsInStore()); + Assert.True(_updateSink.Inits.TryTake(out var initData, TimeSpan.FromMilliseconds(100))); + AssertJsonEqual(DataSetAsJson(ExpectedDataSetForFullDataFile(1)), DataSetAsJson(initData)); } } @@ -105,7 +104,8 @@ public void CanIgnoreMissingFileOnStartup() var task = fp.Start(); Assert.True(task.IsCompleted); Assert.True(fp.Initialized); - Assert.Equal(2, CountFlagsInStore()); + Assert.True(_updateSink.Inits.TryTake(out var initData, TimeSpan.FromMilliseconds(100))); + AssertJsonEqual(DataSetAsJson(ExpectedDataSetForFullDataFile(1)), DataSetAsJson(initData)); } } @@ -131,10 +131,10 @@ public void ModifiedFileIsNotReloadedIfAutoUpdateIsOff() using (var fp = MakeDataSource()) { fp.Start(); + Assert.True(_updateSink.Inits.TryTake(out var initData, TimeSpan.FromMilliseconds(100))); + file.SetContentFromPath(TestUtils.TestFilePath("segment-only.json")); - Thread.Sleep(TimeSpan.FromMilliseconds(400)); - Assert.Equal(1, CountFlagsInStore()); - Assert.Equal(0, CountSegmentsInStore()); + Assert.False(_updateSink.Inits.TryTake(out _, TimeSpan.FromMilliseconds(400))); } } } @@ -149,18 +149,41 @@ public void ModifiedFileIsReloadedIfAutoUpdateIsOn() using (var fp = MakeDataSource()) { fp.Start(); - Assert.True(store.Initialized()); - Assert.Equal(0, CountSegmentsInStore()); - - Thread.Sleep(TimeSpan.FromMilliseconds(1000)); - // See FilePollingReloader for the reason behind this long sleep + Assert.True(_updateSink.Inits.TryTake(out var initData, TimeSpan.FromMilliseconds(100))); + AssertJsonEqual(DataSetAsJson(ExpectedDataSetForFlagOnlyFile(1)), DataSetAsJson(initData)); file.SetContentFromPath(TestUtils.TestFilePath("segment-only.json")); - Assert.True( - WaitForCondition(TimeSpan.FromSeconds(5), () => CountSegmentsInStore() == 1), - "Did not detect file modification" - ); + Assert.True(_updateSink.Inits.TryTake(out var newData, TimeSpan.FromSeconds(5)), "timed out waiting for update"); + + AssertJsonEqual(DataSetAsJson(ExpectedDataSetForSegmentOnlyFile(2)), DataSetAsJson(newData)); + } + } + } + + [Fact] + public void FlagChangeEventIsGeneratedWhenModifiedFileIsReloaded() + { + using (var file = TempFile.Create()) + { + file.SetContent(@"{""flagValues"":{""flag1"":""a""}}"); + + var config = Configuration.Builder("") + .DataSource(FileData.DataSource().FilePaths(file.Path).AutoUpdate(true)) + .Events(Components.NoEvents) + .Logging(testLogging) + .Build(); + + using (var client = new LdClient(config)) + { + var events = new EventSink(); + client.FlagTracker.FlagChanged += events.Add; + + file.SetContent(@"{""flagValues"":{""flag1"":""b""}}"); + + var e = events.ExpectValue(TimeSpan.FromSeconds(5)); + Assert.Equal("flag1", e.Key); + Assert.Equal("b", client.StringVariation("flag1", user, "")); } } } @@ -179,16 +202,13 @@ public void ModifiedFileIsNotReloadedIfOneFileIsMissing() using (var fp = MakeDataSource()) { fp.Start(); - Assert.True(store.Initialized()); - Assert.Equal(0, CountSegmentsInStore()); + Assert.True(_updateSink.Inits.TryTake(out var initData, TimeSpan.FromMilliseconds(100))); + AssertJsonEqual(DataSetAsJson(ExpectedDataSetForFlagOnlyFile(1)), DataSetAsJson(initData)); - Thread.Sleep(TimeSpan.FromMilliseconds(1000)); - // See FilePollingReloader for the reason behind this long sleep file2.Delete(); file1.SetContentFromPath(TestUtils.TestFilePath("segment-only.json")); - Thread.Sleep(TimeSpan.FromMilliseconds(400)); - Assert.Equal(0, CountSegmentsInStore()); + Assert.False(_updateSink.Inits.TryTake(out _, TimeSpan.FromMilliseconds(400)), "got unexpected update"); } } } @@ -207,18 +227,14 @@ public void ModifiedFileIsReloadedEvenIfOneFileIsMissingIfSkipMissingPathsIsSet( using (var fp = MakeDataSource()) { fp.Start(); - Assert.True(store.Initialized()); - Assert.Equal(0, CountSegmentsInStore()); - - Thread.Sleep(TimeSpan.FromMilliseconds(1000)); - // See FilePollingReloader for the reason behind this long sleep + Assert.True(_updateSink.Inits.TryTake(out var initData, TimeSpan.FromMilliseconds(100))); + AssertJsonEqual(DataSetAsJson(ExpectedDataSetForFlagOnlyFile(1)), DataSetAsJson(initData)); file1.SetContentFromPath(TestUtils.TestFilePath("segment-only.json")); - Assert.True( - WaitForCondition(TimeSpan.FromSeconds(3), () => CountSegmentsInStore() == 1), - "Did not detect file modification" - ); + Assert.True(_updateSink.Inits.TryTake(out var newData, TimeSpan.FromSeconds(5)), "timed out waiting for update"); + + AssertJsonEqual(DataSetAsJson(ExpectedDataSetForSegmentOnlyFile(2)), DataSetAsJson(newData)); } } } @@ -233,17 +249,15 @@ public void IfFlagsAreBadAtStartTimeAutoUpdateCanStillLoadGoodDataLater() using (var fp = MakeDataSource()) { fp.Start(); - Assert.False(store.Initialized()); - - Thread.Sleep(TimeSpan.FromMilliseconds(1000)); - // See FilePollingReloader for the reason behind this long sleep + Assert.False(_updateSink.Inits.TryTake(out _, TimeSpan.FromMilliseconds(100))); file.SetContentFromPath(TestUtils.TestFilePath("segment-only.json")); - Assert.True( - WaitForCondition(TimeSpan.FromSeconds(5), () => CountSegmentsInStore() == 1), - "Did not detect file modification" - ); + Assert.True(_updateSink.Inits.TryTake(out var newData, TimeSpan.FromSeconds(5)), "timed out waiting for update"); + + AssertJsonEqual(DataSetAsJson(ExpectedDataSetForSegmentOnlyFile(2)), DataSetAsJson(newData)); + // Note that the expected version is 2 because we increment the version on each + // *attempt* to load the files, not on each successful load. } } } @@ -269,29 +283,35 @@ public void SimplifiedFlagEvaluatesAsExpected() Assert.Equal("value2", client.StringVariation("flag2", user, "")); } } - - private int CountFlagsInStore() - { - return store.GetAll(DataModel.Features).Items.Count(); - } - private int CountSegmentsInStore() - { - return store.GetAll(DataModel.Segments).Items.Count(); - } + private static FullDataSet ExpectedDataSetForFullDataFile(int version) => + new DataSetBuilder() + .Flags( + new FeatureFlagBuilder("flag1").Version(version).On(true).FallthroughVariation(2) + .Variations(LdValue.Of("fall"), LdValue.Of("off"), LdValue.Of("on")).Build(), + new FeatureFlagBuilder("flag2").Version(version).On(true).FallthroughVariation(0) + .Variations(LdValue.Of("value2")).Build() + ) + .Segments( + new SegmentBuilder("seg1").Version(version).Included("user1").Build() + ) + .Build(); - private bool WaitForCondition(TimeSpan maxTime, Func test) - { - DateTime deadline = DateTime.Now.Add(maxTime); - while (DateTime.Now < deadline) - { - if (test()) - { - return true; - } - Thread.Sleep(TimeSpan.FromMilliseconds(100)); - } - return false; - } + private static FullDataSet ExpectedDataSetForFlagOnlyFile(int version) => + new DataSetBuilder() + .Flags( + new FeatureFlagBuilder("flag1").Version(version).On(true).FallthroughVariation(2) + .Variations(LdValue.Of("fall"), LdValue.Of("off"), LdValue.Of("on")).Build() + ) + .Segments() + .Build(); + + private static FullDataSet ExpectedDataSetForSegmentOnlyFile(int version) => + new DataSetBuilder() + .Flags() + .Segments( + new SegmentBuilder("seg1").Version(version).Included("user1").Build() + ) + .Build(); } } diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/FlagFileDataMergerTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/FlagFileDataMergerTest.cs index 77698da5..fb6f85a0 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/FlagFileDataMergerTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/FlagFileDataMergerTest.cs @@ -22,16 +22,8 @@ public void AddToData_DuplicateKeysHandling_Throw() { key, new ItemDescriptor(1, initialFeatureFlag) } }; var segmentData = new Dictionary(); - FlagFileData fileData = new FlagFileData - { - Flags = new Dictionary - { - { - key, - new FeatureFlagBuilder(key).Version(1).Build() - } - } - }; + var fileData = new DataSetBuilder() + .Flags(new FeatureFlagBuilder(key).Version(1).Build()).Build(); FlagFileDataMerger merger = new FlagFileDataMerger(FileDataTypes.DuplicateKeysHandling.Throw); @@ -58,16 +50,8 @@ public void AddToData_DuplicateKeysHandling_Ignore() { key, new ItemDescriptor(1, initialFeatureFlag) } }; var segmentData = new Dictionary(); - FlagFileData fileData = new FlagFileData - { - Flags = new Dictionary - { - { - key, - new FeatureFlagBuilder(key).Version(1).Build() - } - } - }; + var fileData = new DataSetBuilder() + .Flags(new FeatureFlagBuilder(key).Version(1).Build()).Build(); FlagFileDataMerger merger = new FlagFileDataMerger(FileDataTypes.DuplicateKeysHandling.Ignore); merger.AddToData(fileData, flagData, segmentData); diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/StreamProcessorTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/StreamProcessorTest.cs index d1e62876..be21533a 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/StreamProcessorTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataSources/StreamProcessorTest.cs @@ -3,11 +3,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using LaunchDarkly.Sdk.Internal; using LaunchDarkly.Sdk.Internal.Events; using LaunchDarkly.Sdk.Json; using LaunchDarkly.Sdk.Server.Interfaces; using LaunchDarkly.Sdk.Server.Internal.DataStores; using LaunchDarkly.Sdk.Server.Internal.Model; +using LaunchDarkly.TestHelpers; using LaunchDarkly.EventSource; using Moq; using Xunit; @@ -47,12 +49,12 @@ public StreamProcessorTest(ITestOutputHelper testOutput) : base(testOutput) _eventSource = _mockEventSource.Object; _eventSourceFactory = new TestEventSourceFactory(_eventSource); _dataStore = new DelegatingDataStoreForStreamTests { WrappedStore = new InMemoryDataStore() }; - _dataStoreUpdates = new DataStoreUpdatesImpl(new TaskExecutor(testLogger)); + _dataStoreUpdates = new DataStoreUpdatesImpl(new TaskExecutor(null, testLogger), testLogger); _dataStoreStatusProvider = new DataStoreStatusProviderImpl(_dataStore, _dataStoreUpdates); _dataSourceUpdates = new DataSourceUpdatesImpl( _dataStore, _dataStoreStatusProvider, - new TaskExecutor(testLogger), + new TaskExecutor(null, testLogger), testLogger, null ); @@ -277,7 +279,7 @@ public void StreamInitDiagnosticRecordedOnOpen() var diagnosticStore = mockDiagnosticStore.Object; var basicConfig = new BasicConfiguration(SDK_KEY, false, testLogger); var context = new LdClientContext(basicConfig, Components.HttpConfiguration().CreateHttpConfiguration(basicConfig), - diagnosticStore, new TaskExecutor(testLogger)); + diagnosticStore, new TaskExecutor(null, testLogger)); using (var sp = (StreamProcessor)Components.StreamingDataSource().EventSourceCreator(_eventSourceFactory.Create()) .CreateDataSource(context, _dataSourceUpdates)) @@ -302,7 +304,7 @@ public void StreamInitDiagnosticRecordedOnError() var diagnosticStore = mockDiagnosticStore.Object; var basicConfig = new BasicConfiguration(SDK_KEY, false, testLogger); var context = new LdClientContext(basicConfig, Components.HttpConfiguration().CreateHttpConfiguration(basicConfig), - diagnosticStore, new TaskExecutor(testLogger)); + diagnosticStore, new TaskExecutor(null, testLogger)); using (var sp = (StreamProcessor)Components.StreamingDataSource().EventSourceCreator(_eventSourceFactory.Create()) .CreateDataSource(context, _dataSourceUpdates)) diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreStatusProviderImplTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreStatusProviderImplTest.cs index ba1ca7a5..9ec456e7 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreStatusProviderImplTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreStatusProviderImplTest.cs @@ -1,5 +1,7 @@ using System; +using LaunchDarkly.Sdk.Internal; using LaunchDarkly.Sdk.Server.Interfaces; +using LaunchDarkly.TestHelpers; using Xunit; using Xunit.Abstractions; @@ -13,7 +15,7 @@ public class DataStoreStatusProviderImplTest : BaseTest public DataStoreStatusProviderImplTest(ITestOutputHelper testOutput) : base(testOutput) { - _dataStoreUpdates = new DataStoreUpdatesImpl(new TaskExecutor(testLogger)); + _dataStoreUpdates = new DataStoreUpdatesImpl(new TaskExecutor(this, testLogger), testLogger); _dataStoreStatusProvider = new DataStoreStatusProviderImpl(_dataStore, _dataStoreUpdates); } diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreTestBase.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreTestBase.cs index f63bacc8..7f0077e4 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreTestBase.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreTestBase.cs @@ -77,7 +77,7 @@ public void GetAllUnknownKind() { InitStore(); var result = store.GetAll(OtherDataKind); - Assert.Equal(0, result.Items.Count()); + Assert.Empty(result.Items); } [Fact] diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreUpdatesImplTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreUpdatesImplTest.cs index 0f29fa57..ef65dc27 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreUpdatesImplTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/DataStoreUpdatesImplTest.cs @@ -1,4 +1,6 @@ -using LaunchDarkly.Sdk.Server.Interfaces; +using LaunchDarkly.Sdk.Internal; +using LaunchDarkly.Sdk.Server.Interfaces; +using LaunchDarkly.TestHelpers; using Xunit; using Xunit.Abstractions; @@ -10,7 +12,7 @@ public class DataStoreUpdatesImplTest : BaseTest public DataStoreUpdatesImplTest(ITestOutputHelper testOutput) : base(testOutput) { - updates = new DataStoreUpdatesImpl(new TaskExecutor(testLogger)); + updates = new DataStoreUpdatesImpl(new TaskExecutor(this, testLogger), testLogger); } [Fact] diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/PersistentStoreWrapperTestBase.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/PersistentStoreWrapperTestBase.cs index 8e3d1a29..48489a10 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/PersistentStoreWrapperTestBase.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/DataStores/PersistentStoreWrapperTestBase.cs @@ -4,7 +4,9 @@ using System.Linq; using System.Threading; using LaunchDarkly.Logging; +using LaunchDarkly.Sdk.Internal; using LaunchDarkly.Sdk.Server.Interfaces; +using LaunchDarkly.TestHelpers; using Xunit; using Xunit.Abstractions; @@ -87,8 +89,8 @@ public static IEnumerable AllTestParams() protected PersistentStoreWrapperTestBase(T core, ITestOutputHelper testOutput) : base(testOutput) { _core = core; - _taskExecutor = new TaskExecutor(testLogger); - _dataStoreUpdates = new DataStoreUpdatesImpl(_taskExecutor); + _taskExecutor = new TaskExecutor(this, testLogger); + _dataStoreUpdates = new DataStoreUpdatesImpl(_taskExecutor, testLogger); } internal abstract PersistentStoreWrapper MakeWrapper(TestParams testParams); diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/Events/ServerDiagnosticStoreTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/Events/ServerDiagnosticStoreTest.cs index f552f69c..024a3668 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/Events/ServerDiagnosticStoreTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/Events/ServerDiagnosticStoreTest.cs @@ -99,7 +99,7 @@ public void CanAddStreamInit() LdValue streamInit = streamInits.Get(0); Assert.Equal(UnixMillisecondTime.FromDateTime(timestamp).Value, streamInit.Get("timestamp").AsLong); Assert.Equal(200, streamInit.Get("durationMillis").AsInt); - Assert.Equal(true, streamInit.Get("failed").AsBool); + Assert.True(streamInit.Get("failed").AsBool); } [Fact] diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/FlagTrackerImplTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/FlagTrackerImplTest.cs index 78e22c4f..9dae6ab3 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/FlagTrackerImplTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/Internal/FlagTrackerImplTest.cs @@ -3,6 +3,7 @@ using LaunchDarkly.Sdk.Server.Interfaces; using LaunchDarkly.Sdk.Server.Internal.DataStores; using LaunchDarkly.Sdk.Server.Internal.Model; +using LaunchDarkly.TestHelpers; using Xunit; using Xunit.Abstractions; diff --git a/test/LaunchDarkly.ServerSdk.Tests/Internal/TaskExecutorTest.cs b/test/LaunchDarkly.ServerSdk.Tests/Internal/TaskExecutorTest.cs deleted file mode 100644 index 52a269d0..00000000 --- a/test/LaunchDarkly.ServerSdk.Tests/Internal/TaskExecutorTest.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -using LaunchDarkly.Logging; -using Xunit; -using Xunit.Abstractions; - -namespace LaunchDarkly.Sdk.Server.Internal -{ - public class TaskExecutorTest : BaseTest - { - private readonly TaskExecutor executor; - private event EventHandler myEvent; - - public TaskExecutorTest(ITestOutputHelper testOutput) : base(testOutput) - { - executor = new TaskExecutor(testLogger); - } - - [Fact] - public void SendsEvent() - { - var values1 = new EventSink(); - var values2 = new EventSink(); - myEvent += values1.Add; - myEvent += values2.Add; - - executor.ScheduleEvent(this, "hello", myEvent); - - Assert.Equal("hello", values1.ExpectValue()); - Assert.Equal("hello", values2.ExpectValue()); - } - - [Fact] - public void ExceptionFromEventHandlerIsLoggedAndDoesNotStopOtherHandlers() - { - var values1 = new EventSink(); - myEvent += (sender, args) => throw new Exception("sorry"); - myEvent += values1.Add; - - executor.ScheduleEvent(this, "hello", myEvent); - - Assert.Equal("hello", values1.ExpectValue()); - - AssertEventually(TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(20), () => - logCapture.HasMessageWithText(LogLevel.Error, "Unexpected exception from event handler: System.Exception: sorry") && - logCapture.HasMessageWithRegex(LogLevel.Debug, "at LaunchDarkly.Sdk.Server.Internal.TaskExecutorTest")); - } - - [Fact] - public void RepeatingTask() - { - var values = new BlockingCollection(); - var testGate = new EventWaitHandle(false, EventResetMode.AutoReset); - var nextValue = 1; - var canceller = executor.StartRepeatingTask(TimeSpan.Zero, TimeSpan.FromMilliseconds(100), async () => - { - testGate.WaitOne(); - values.Add(nextValue++); - await Task.FromResult(true); // an arbitrary await just to make this function async - }); - - testGate.Set(); - Assert.True(values.TryTake(out var value1, TimeSpan.FromSeconds(2))); - Assert.Equal(1, value1); - - testGate.Set(); - Assert.True(values.TryTake(out var value2, TimeSpan.FromSeconds(2))); - Assert.Equal(2, value2); - - canceller.Cancel(); - testGate.Set(); - Assert.False(values.TryTake(out _, TimeSpan.FromMilliseconds(200))); - } - - [Fact] - public void ExceptionFromRepeatingTaskIsLoggedAndDoesNotStopTask() - { - var values = new BlockingCollection(); - var testGate = new EventWaitHandle(false, EventResetMode.AutoReset); - var nextValue = 1; - var canceller = executor.StartRepeatingTask(TimeSpan.Zero, TimeSpan.FromMilliseconds(100), async () => - { - testGate.WaitOne(); - var valueWas = nextValue++; - if (valueWas == 1) - { - throw new Exception("sorry"); - } - else - { - values.Add(valueWas++); - } - await Task.FromResult(true); // an arbitrary await just to make this function async - }); - - testGate.Set(); - Assert.False(values.TryTake(out _, TimeSpan.FromMilliseconds(100))); - - AssertEventually(TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(20), () => - logCapture.HasMessageWithText(LogLevel.Error, "Unexpected exception from repeating task: System.Exception: sorry") && - logCapture.HasMessageWithRegex(LogLevel.Debug, "at LaunchDarkly.Sdk.Server.Internal.TaskExecutorTest")); - - testGate.Set(); - Assert.True(values.TryTake(out var value2, TimeSpan.FromSeconds(2))); - Assert.Equal(2, value2); - - canceller.Cancel(); - testGate.Set(); - } - - private static void AssertEventually(TimeSpan timeout, TimeSpan interval, Func test) - { - var deadline = DateTime.Now.Add(timeout); - while (DateTime.Now < deadline) - { - if (test()) - { - return; - } - Thread.Sleep(interval); - } - Assert.True(false, "timed out before test condition was satisfied"); - } - } -} diff --git a/test/LaunchDarkly.ServerSdk.Tests/LaunchDarkly.ServerSdk.Tests.csproj b/test/LaunchDarkly.ServerSdk.Tests/LaunchDarkly.ServerSdk.Tests.csproj index 955eef9a..6e51e678 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/LaunchDarkly.ServerSdk.Tests.csproj +++ b/test/LaunchDarkly.ServerSdk.Tests/LaunchDarkly.ServerSdk.Tests.csproj @@ -4,7 +4,7 @@ single framework that we are testing; this allows us to test with older SDK versions that would error out if they saw any newer target frameworks listed here, even if we weren't running those. --> - netcoreapp2.1;net452 + netcoreapp2.1;net452;net5.0 $(TESTFRAMEWORK) LaunchDarkly.ServerSdk.Tests LaunchDarkly.ServerSdk.Tests @@ -18,11 +18,11 @@ - - + + - - + + diff --git a/test/LaunchDarkly.ServerSdk.Tests/LdClientDiagnosticEventTest.cs b/test/LaunchDarkly.ServerSdk.Tests/LdClientDiagnosticEventTest.cs index 70606ad9..e05151fd 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/LdClientDiagnosticEventTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/LdClientDiagnosticEventTest.cs @@ -6,10 +6,13 @@ using LaunchDarkly.Sdk.Server.Interfaces; using LaunchDarkly.Sdk.Server.Internal.DataStores; using LaunchDarkly.Sdk.Server.Internal.Events; +using LaunchDarkly.TestHelpers; using Xunit; using Xunit.Abstractions; using static LaunchDarkly.Sdk.Server.TestHttpUtils; +using static LaunchDarkly.TestHelpers.JsonAssertions; +using static LaunchDarkly.TestHelpers.JsonTestValue; namespace LaunchDarkly.Sdk.Server { @@ -18,12 +21,12 @@ public class LdClientDiagnosticEventTest : BaseTest private const string sdkKey = "SDK_KEY"; private const string testWrapperName = "wrapper-name"; private const string testWrapperVersion = "1.2.3"; - private static readonly LdValue expectedSdk = LdValue.BuildObject() + private static readonly JsonTestValue expectedSdk = JsonOf(LdValue.BuildObject() .Add("name", "dotnet-server-sdk") .Add("version", AssemblyVersions.GetAssemblyVersionStringForType(typeof(LdClient))) .Add("wrapperName", testWrapperName) .Add("wrapperVersion", testWrapperVersion) - .Build(); + .Build().ToJsonString()); internal static readonly TimeSpan testStartWaitTime = TimeSpan.FromMilliseconds(1); private TestEventSender testEventSender = new TestEventSender(); @@ -63,26 +66,23 @@ public void DiagnosticInitEventIsSent() Assert.Equal(EventDataKind.DiagnosticEvent, payload.Kind); Assert.Equal(1, payload.EventCount); - var data = LdValue.Parse(payload.Data); - Assert.Equal("diagnostic-init", data.Get("kind").AsString); - AssertHelpers.JsonEqual(ExpectedPlatform(), data.Get("platform")); - AssertHelpers.JsonEqual(expectedSdk, data.Get("sdk")); - Assert.Equal("DK_KEY", data.Get("id").Get("sdkKeySuffix").AsString); + var data = JsonOf(payload.Data); + AssertJsonEqual(JsonFromValue("diagnostic-init"), data.Property("kind")); + AssertJsonEqual(ExpectedPlatform(), data.Property("platform")); + AssertJsonEqual(expectedSdk, data.Property("sdk")); + AssertJsonEqual(JsonFromValue("DK_KEY"), data.Property("id").Property("sdkKeySuffix")); - var timestamp = data.Get("creationDate").AsLong; - Assert.NotEqual(0, timestamp); + data.RequiredProperty("creationDate"); } } - private static LdValue ExpectedPlatform() - { - return LdValue.BuildObject().Add("name", "dotnet") + private static JsonTestValue ExpectedPlatform() => + JsonOf(LdValue.BuildObject().Add("name", "dotnet") .Add("dotNetTargetFramework", ServerDiagnosticStore.GetDotNetTargetFramework()) .Add("osName", ServerDiagnosticStore.GetOSName()) .Add("osVersion", ServerDiagnosticStore.GetOSVersion()) .Add("osArch", ServerDiagnosticStore.GetOSArch()) - .Build(); - } + .Build().ToJsonString()); [Fact] public void DiagnosticPeriodicEventsAreSent() @@ -469,10 +469,10 @@ LdValue.ObjectBuilder expected Assert.Equal(EventDataKind.DiagnosticEvent, payload.Kind); Assert.Equal(1, payload.EventCount); - var data = LdValue.Parse(payload.Data); - Assert.Equal("diagnostic-init", data.Get("kind").AsString); + var data = JsonOf(payload.Data); + AssertJsonEqual(JsonFromValue("diagnostic-init"), data.Property("kind")); - AssertHelpers.JsonEqual(expected.Build(), data.Get("configuration")); + AssertJsonEqual(JsonOf(expected.Build().ToJsonString()), data.Property("configuration")); } } diff --git a/test/LaunchDarkly.ServerSdk.Tests/LdClientEvaluationTest.cs b/test/LaunchDarkly.ServerSdk.Tests/LdClientEvaluationTest.cs index e5b3a526..500bad29 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/LdClientEvaluationTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/LdClientEvaluationTest.cs @@ -5,6 +5,7 @@ using LaunchDarkly.Sdk.Server.Interfaces; using LaunchDarkly.Sdk.Server.Internal.Evaluation; using LaunchDarkly.Sdk.Server.Internal.Model; +using LaunchDarkly.TestHelpers; using Moq; using Xunit; using Xunit.Abstractions; @@ -49,7 +50,7 @@ public void BoolVariationReturnsDefaultValueForWrongType() { testData.UsePreconfiguredFlag(new FeatureFlagBuilder("key").OffWithValue(LdValue.Of("wrong")).Build()); - Assert.Equal(false, client.BoolVariation("key", user, false)); + Assert.False(client.BoolVariation("key", user, false)); } [Fact] @@ -349,7 +350,7 @@ public void AllFlagsStateReturnsState() ""$valid"":true }"; var actualString = LdJsonSerialization.SerializeObject(state); - AssertHelpers.JsonEqual(expectedString, actualString); + JsonAssertions.AssertJsonEqual(expectedString, actualString); } [Fact] @@ -379,7 +380,7 @@ public void AllFlagsStateReturnsStateWithReasons() ""$valid"":true }"; var actualString = LdJsonSerialization.SerializeObject(state); - AssertHelpers.JsonEqual(expectedString, actualString); + JsonAssertions.AssertJsonEqual(expectedString, actualString); } [Fact] @@ -441,7 +442,7 @@ public void AllFlagsStateCanOmitDetailsForUntrackedFlags() ""$valid"":true }"; var actualString = LdJsonSerialization.SerializeObject(state); - AssertHelpers.JsonEqual(expectedString, actualString); + JsonAssertions.AssertJsonEqual(expectedString, actualString); } [Fact] diff --git a/test/LaunchDarkly.ServerSdk.Tests/LdClientEventTest.cs b/test/LaunchDarkly.ServerSdk.Tests/LdClientEventTest.cs index e67c2d20..62fa53e3 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/LdClientEventTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/LdClientEventTest.cs @@ -30,7 +30,7 @@ public void IdentifySendsEvent() { client.Identify(user); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); var ie = Assert.IsType(eventSink.Events[0]); Assert.Equal(user.Key, ie.User.Key); } @@ -40,7 +40,7 @@ public void IdentifyWithNoUserSendsNoEvent() { client.Identify(null); - Assert.Equal(0, eventSink.Events.Count); + Assert.Empty(eventSink.Events); } [Fact] @@ -48,7 +48,7 @@ public void IdentifyWithNoUserKeySendsNoEvent() { client.Identify(User.WithKey(null)); - Assert.Equal(0, eventSink.Events.Count); + Assert.Empty(eventSink.Events); } [Fact] @@ -56,7 +56,7 @@ public void IdentifyWithEmptyUserKeySendsNoEvent() { client.Identify(User.WithKey("")); - Assert.Equal(0, eventSink.Events.Count); + Assert.Empty(eventSink.Events); } [Fact] @@ -64,7 +64,7 @@ public void TrackSendsEventWithoutData() { client.Track("eventkey", user); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); var ce = Assert.IsType(eventSink.Events[0]); Assert.Equal(user.Key, ce.User.Key); Assert.Equal("eventkey", ce.EventKey); @@ -78,7 +78,7 @@ public void TrackSendsEventWithData() var data = LdValue.BuildObject().Add("thing", "stuff").Build(); client.Track("eventkey", user, data); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); var ce = Assert.IsType(eventSink.Events[0]); Assert.Equal(user.Key, ce.User.Key); Assert.Equal("eventkey", ce.EventKey); @@ -91,7 +91,7 @@ public void TrackSendsEventWithWithMetricValue() var data = LdValue.BuildObject().Add("thing", "stuff").Build(); client.Track("eventkey", user, data, 1.5); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); var ce = Assert.IsType(eventSink.Events[0]); Assert.Equal(user.Key, ce.User.Key); Assert.Equal("eventkey", ce.EventKey); @@ -104,7 +104,7 @@ public void TrackWithNoUserSendsNoEvent() { client.Track("eventkey", null); - Assert.Equal(0, eventSink.Events.Count); + Assert.Empty(eventSink.Events); } [Fact] @@ -112,7 +112,7 @@ public void TrackWithNullUserKeySendsNoEvent() { client.Track("eventkey", User.WithKey(null)); - Assert.Equal(0, eventSink.Events.Count); + Assert.Empty(eventSink.Events); } [Fact] @@ -120,7 +120,7 @@ public void TrackWithEmptyUserKeySendsNoEvent() { client.Track("eventkey", User.WithKey("")); - Assert.Equal(0, eventSink.Events.Count); + Assert.Empty(eventSink.Events); } [Fact] @@ -130,7 +130,7 @@ public void BoolVariationSendsEvent() testData.UsePreconfiguredFlag(flag); client.BoolVariation("key", user, false); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckFeatureEvent(eventSink.Events[0], flag, LdValue.Of(true), LdValue.Of(false), null); } @@ -138,7 +138,7 @@ public void BoolVariationSendsEvent() public void BoolVariationSendsEventForUnknownFlag() { client.BoolVariation("key", user, false); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckUnknownFeatureEvent(eventSink.Events[0], "key", LdValue.Of(false), null); } @@ -149,7 +149,7 @@ public void IntVariationSendsEvent() testData.UsePreconfiguredFlag(flag); client.IntVariation("key", user, 1); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckFeatureEvent(eventSink.Events[0], flag, LdValue.Of(2), LdValue.Of(1), null); } @@ -157,7 +157,7 @@ public void IntVariationSendsEvent() public void IntVariationSendsEventForUnknownFlag() { client.IntVariation("key", user, 1); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckUnknownFeatureEvent(eventSink.Events[0], "key", LdValue.Of(1), null); } @@ -168,7 +168,7 @@ public void FloatVariationSendsEvent() testData.UsePreconfiguredFlag(flag); client.FloatVariation("key", user, 1.0f); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckFeatureEvent(eventSink.Events[0], flag, LdValue.Of(2.5f), LdValue.Of(1.0f), null); } @@ -176,7 +176,7 @@ public void FloatVariationSendsEvent() public void FloatVariationSendsEventForUnknownFlag() { client.FloatVariation("key", user, 1.0f); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckUnknownFeatureEvent(eventSink.Events[0], "key", LdValue.Of(1.0f), null); } @@ -187,7 +187,7 @@ public void StringVariationSendsEvent() testData.UsePreconfiguredFlag(flag); client.StringVariation("key", user, "a"); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckFeatureEvent(eventSink.Events[0], flag, LdValue.Of("b"), LdValue.Of("a"), null); } @@ -195,7 +195,7 @@ public void StringVariationSendsEvent() public void StringVariationSendsEventForUnknownFlag() { client.StringVariation("key", user, "a"); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckUnknownFeatureEvent(eventSink.Events[0], "key", LdValue.Of("a"), null); } @@ -208,7 +208,7 @@ public void JsonVariationSendsEvent() var defaultVal = LdValue.Of(42); client.JsonVariation("key", user, defaultVal); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckFeatureEvent(eventSink.Events[0], flag, data, defaultVal, null); } @@ -218,7 +218,7 @@ public void JsonVariationSendsEventForUnknownFlag() var defaultVal = LdValue.Of(42); client.JsonVariation("key", user, defaultVal); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckUnknownFeatureEvent(eventSink.Events[0], "key", defaultVal, null); } @@ -240,7 +240,7 @@ public void EventTrackingAndReasonCanBeForcedForRule() // Note, we did not call StringVariationDetail and the flag is not tracked, but we should still get // tracking and a reason, because the rule-level trackEvents flag is on for the matched rule. - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); var e = Assert.IsType(eventSink.Events[0]); Assert.True(e.TrackEvents); Assert.Equal(EvaluationReason.RuleMatchReason(0, "rule-id"), e.Reason); @@ -265,7 +265,7 @@ public void EventTrackingAndReasonAreNotForcedIfFlagIsNotSetForMatchingRule() // It matched rule1, which has trackEvents: false, so we don't get the override behavior - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); var e = Assert.IsType(eventSink.Events[0]); Assert.False(e.TrackEvents); Assert.Null(e.Reason); @@ -288,7 +288,7 @@ public void EventTrackingAndReasonCanBeForcedForFallthrough() // Note, we did not call stringVariationDetail and the flag is not tracked, but we should still get // tracking and a reason, because trackEventsFallthrough is on and the evaluation fell through. - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); var e = Assert.IsType(eventSink.Events[0]); Assert.True(e.TrackEvents); Assert.Equal(EvaluationReason.FallthroughReason, e.Reason); @@ -307,7 +307,7 @@ public void EventTrackingAndReasonAreNotForcedForFallthroughIfFlagIsNotSet() client.StringVariation("flag", user, "default"); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); var e = Assert.IsType(eventSink.Events[0]); Assert.False(e.TrackEvents); Assert.Null(e.Reason); @@ -355,7 +355,7 @@ public void EventIsSentWithDefaultValueForFlagThatEvaluatesToNull() var result = client.StringVariation(flag.Key, user, defaultVal); Assert.Equal(defaultVal, result); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckFeatureEvent(eventSink.Events[0], flag, LdValue.Of(defaultVal), LdValue.Of(defaultVal), null); } @@ -374,7 +374,7 @@ public void EventIsNotSentForUnknownPrerequisiteFlag() client.StringVariation("feature0", user, "default"); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); CheckFeatureEvent(eventSink.Events[0], f0, LdValue.Of("off"), LdValue.Of("default"), null); } @@ -383,7 +383,7 @@ public void AliasSendsEvent() { client.Alias(User.WithKey("current"), User.Builder("previous").Anonymous(true).Build()); - Assert.Equal(1, eventSink.Events.Count); + Assert.Single(eventSink.Events); var e = Assert.IsType(eventSink.Events[0]); Assert.Equal("current", e.CurrentKey); Assert.Equal(ContextKind.User, e.CurrentKind); diff --git a/test/LaunchDarkly.ServerSdk.Tests/LdClientListenersTest.cs b/test/LaunchDarkly.ServerSdk.Tests/LdClientListenersTest.cs index 3f856582..770226e2 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/LdClientListenersTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/LdClientListenersTest.cs @@ -1,6 +1,7 @@ using System; using LaunchDarkly.Sdk.Server.Integrations; using LaunchDarkly.Sdk.Server.Interfaces; +using LaunchDarkly.TestHelpers; using Moq; using Xunit; using Xunit.Abstractions; @@ -277,5 +278,30 @@ public void BigSegmentStoreStatusProviderSendsStatusUpdates() Assert.False(status2.Available); } } + + [Fact] + public void EventSenderIsClientInstance() + { + // We're only checking one kind of events here (FlagChanged), but since the SDK uses the + // same TaskExecutor instance for all event dispatches and the sender is configured in + // that object, the sender should be the same for all events. + + var flagKey = "flagKey"; + var testData = TestData.DataSource(); + testData.Update(testData.Flag(flagKey).On(true)); + var config = Configuration.Builder("").DataSource(testData) + .Events(Components.NoEvents).Build(); + + using (var client = new LdClient(config)) + { + var receivedSender = new EventSink(); + client.FlagTracker.FlagChanged += (s, e) => receivedSender.Enqueue(s); + + testData.Update(testData.Flag(flagKey).On(false)); + + var sender = receivedSender.ExpectValue(); + Assert.Same(client, sender); + } + } } } diff --git a/test/LaunchDarkly.ServerSdk.Tests/LdClientOfflineTest.cs b/test/LaunchDarkly.ServerSdk.Tests/LdClientOfflineTest.cs index 7dea9787..b0ef651b 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/LdClientOfflineTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/LdClientOfflineTest.cs @@ -70,7 +70,7 @@ public void OfflineClientGetsFlagFromDataStore() .Build(); using (var client = new LdClient(config)) { - Assert.Equal(true, client.BoolVariation("key", User.WithKey("user"), false)); + Assert.True(client.BoolVariation("key", User.WithKey("user"), false)); } } diff --git a/test/LaunchDarkly.ServerSdk.Tests/LdClientTest.cs b/test/LaunchDarkly.ServerSdk.Tests/LdClientTest.cs index 09a3e277..47bb3f5a 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/LdClientTest.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/LdClientTest.cs @@ -9,6 +9,7 @@ using LaunchDarkly.Sdk.Server.Internal.DataStores; using LaunchDarkly.Sdk.Server.Internal.Events; using LaunchDarkly.Sdk.Server.Internal.Model; +using LaunchDarkly.TestHelpers; using Moq; using Xunit; using Xunit.Abstractions; @@ -316,12 +317,10 @@ public void DataSetIsPassedToDataStoreInCorrectOrder() var mockStore = new Mock(); var store = mockStore.Object; - FullDataSet receivedData = new FullDataSet(); + var dataSink = new EventSink>(); mockStore.Setup(s => s.Init(It.IsAny>())) - .Callback((FullDataSet data) => { - receivedData = data; - }); + .Callback((FullDataSet data) => dataSink.Enqueue(data)); mockDataSource.Setup(up => up.Start()).Returns(initTask); @@ -334,7 +333,7 @@ public void DataSetIsPassedToDataStoreInCorrectOrder() using (var client = new LdClient(config)) { - Assert.NotNull(receivedData); + var receivedData = dataSink.ExpectValue(); DataStoreSorterTest.VerifyDataSetOrder(receivedData, DataStoreSorterTest.DependencyOrderingTestData, DataStoreSorterTest.ExpectedOrderingForSortedDataSet); } diff --git a/test/LaunchDarkly.ServerSdk.Tests/TestFiles/all-properties.json b/test/LaunchDarkly.ServerSdk.Tests/TestFiles/all-properties.json index 18c554a4..e4fe0bad 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/TestFiles/all-properties.json +++ b/test/LaunchDarkly.ServerSdk.Tests/TestFiles/all-properties.json @@ -17,7 +17,7 @@ "seg1": { "key": "seg1", "version": 1, - "include": [ "user1" ] + "included": [ "user1" ] } } } diff --git a/test/LaunchDarkly.ServerSdk.Tests/TestFiles/all-properties.yml b/test/LaunchDarkly.ServerSdk.Tests/TestFiles/all-properties.yml index 634096b7..51e9f365 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/TestFiles/all-properties.yml +++ b/test/LaunchDarkly.ServerSdk.Tests/TestFiles/all-properties.yml @@ -16,4 +16,4 @@ segments: seg1: key: seg1 version: 1 - include: ["user1"] + included: ["user1"] diff --git a/test/LaunchDarkly.ServerSdk.Tests/TestFiles/segment-only.json b/test/LaunchDarkly.ServerSdk.Tests/TestFiles/segment-only.json index f20e9856..667f00e8 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/TestFiles/segment-only.json +++ b/test/LaunchDarkly.ServerSdk.Tests/TestFiles/segment-only.json @@ -3,7 +3,7 @@ "seg1": { "key": "seg1", "version": 1, - "include": [ "user1" ] + "included": [ "user1" ] } } } diff --git a/test/LaunchDarkly.ServerSdk.Tests/TestLogging.cs b/test/LaunchDarkly.ServerSdk.Tests/TestLogging.cs index 22e3c5a8..c40da17e 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/TestLogging.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/TestLogging.cs @@ -28,7 +28,16 @@ public class TestLogging /// class constructor /// a log adapter public static ILogAdapter TestOutputAdapter(ITestOutputHelper testOutputHelper) => - Logs.ToMethod(line => testOutputHelper.WriteLine("LOG OUTPUT >> " + line)); + Logs.ToMethod(line => + { + // ITestOutputHelper.WriteLine can throw an exception if we try to write output after the + // end of a test (for instance, from a worker task). We can ignore any such errors. + try + { + testOutputHelper.WriteLine("LOG OUTPUT >> " + line); + } + catch { } + }); /// /// Creates a that sends logging to the Xunit output buffer. Use this when testing diff --git a/test/LaunchDarkly.ServerSdk.Tests/TestUtils.cs b/test/LaunchDarkly.ServerSdk.Tests/TestUtils.cs index 56e79713..b76f009b 100644 --- a/test/LaunchDarkly.ServerSdk.Tests/TestUtils.cs +++ b/test/LaunchDarkly.ServerSdk.Tests/TestUtils.cs @@ -4,16 +4,16 @@ using System.IO; using System.Linq; using LaunchDarkly.Logging; +using LaunchDarkly.Sdk.Internal; using LaunchDarkly.Sdk.Server.Interfaces; -using LaunchDarkly.Sdk.Server.Internal; using LaunchDarkly.Sdk.Server.Internal.DataSources; using LaunchDarkly.Sdk.Server.Internal.DataStores; +using LaunchDarkly.Sdk.Internal.Events; using LaunchDarkly.Sdk.Server.Internal.Model; +using LaunchDarkly.TestHelpers; using System.Threading.Tasks; -using Xunit; using static LaunchDarkly.Sdk.Server.Interfaces.DataStoreTypes; -using LaunchDarkly.Sdk.Internal.Events; namespace LaunchDarkly.Sdk.Server { @@ -71,8 +71,8 @@ internal static IDataSourceFactory DataSourceWithData(FullDataSet new DataSourceUpdatesImpl( dataStore, - new DataStoreStatusProviderImpl(dataStore, new DataStoreUpdatesImpl(new TaskExecutor(logger))), - new TaskExecutor(logger), + new DataStoreStatusProviderImpl(dataStore, new DataStoreUpdatesImpl(new TaskExecutor(null, logger), logger)), + new TaskExecutor(null, logger), logger, null ); @@ -91,6 +91,21 @@ internal static FullDataSet NormalizeDataSet(FullDataSet data) + { + var ob0 = LdValue.BuildObject(); + foreach (var kv0 in data.Data) + { + var ob1 = LdValue.BuildObject(); + foreach (var kv1 in kv0.Value.Items) + { + ob1.Add(kv1.Key, LdValue.Parse(kv0.Key.Serialize(kv1.Value))); + } + ob0.Add(kv0.Key.Name, ob1.Build()); + } + return JsonTestValue.JsonOf(ob0.Build().ToJsonString()); + } } public class SpecificDataStoreFactory : IDataStoreFactory @@ -297,39 +312,6 @@ public void RequireNoPayloadSent(TimeSpan timeout) } } - public class EventSink - { - private readonly BlockingCollection _queue = new BlockingCollection(); - - public void Add(object sender, T args) => _queue.Add(args); - - public T ExpectValue() => ExpectValue(TimeSpan.FromSeconds(1)); - - public T ExpectValue(TimeSpan timeout) - { - if (!_queue.TryTake(out var value, timeout)) - { - Assert.True(false, "expected an event but did not get one at " + TestLogging.TimestampString); - } - return value; - } - - public bool TryTakeValue(out T value) - { - return _queue.TryTake(out value, TimeSpan.FromSeconds(1)); - } - - public void ExpectNoValue() => ExpectNoValue(TimeSpan.FromMilliseconds(100)); - - public void ExpectNoValue(TimeSpan timeout) - { - if (_queue.TryTake(out _, timeout)) - { - Assert.False(true, "expected no event but got one at " + TestLogging.TimestampString); - } - } - } - public class TempFile : IDisposable { public string Path { get; }