From e116f46982ce747b767a0a5c912504a135ab787e Mon Sep 17 00:00:00 2001 From: "Andrew S. Brown" Date: Tue, 10 Jul 2018 18:04:28 -0700 Subject: [PATCH 01/47] Remove old circle.yml file --- circle.yml | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 circle.yml diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 7cfe8890..00000000 --- a/circle.yml +++ /dev/null @@ -1,22 +0,0 @@ -dependencies: - pre: - - curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg - - sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg - - sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-trusty-prod trusty main" > /etc/apt/sources.list.d/dotnetdev.list' - - sudo apt-get install apt-transport-https - - sudo apt-get update - - sudo apt-get install dotnet-sdk-2.0.0 - - sudo apt-get install xsltproc - override: - - dotnet restore - - dotnet build src/LaunchDarkly.Common -f netstandard1.6 - - dotnet build src/LaunchDarkly.Common -f netstandard2.0 - - dotnet build src/LaunchDarkly.Xamarin -f netstandard1.6 - - dotnet build src/LaunchDarkly.Xamarin -f netstandard2.0 -test: - override: - - dotnet add tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj package dotnet-xunit - - dotnet restore - - mkdir -p $CIRCLE_TEST_REPORTS/junit - - cd tests/LaunchDarkly.Xamarin.Tests; dotnet xunit -xml xunit.xml - - xsltproc -o $CIRCLE_TEST_REPORTS/junit/junit.xml tests/LaunchDarkly.Xamarin.Tests/xunit-to-junit.xslt tests/LaunchDarkly.Xamarin.Tests/xunit.xml From a03fa0d1b37b01ba7faca546f348f393393fe8a0 Mon Sep 17 00:00:00 2001 From: Andrew Shannon Brown Date: Thu, 12 Jul 2018 11:24:57 -0700 Subject: [PATCH 02/47] Send back flagVersion in events when it is present (#5) Also allow users to have empty string keys --- src/LaunchDarkly.Xamarin/FeatureFlag.cs | 48 ++-- src/LaunchDarkly.Xamarin/LdClient.cs | 12 +- .../FeatureFlagTests.cs | 29 +++ .../FlagCacheManagerTests.cs | 212 +++++++++--------- .../LdClientTests.cs | 86 +++---- 5 files changed, 210 insertions(+), 177 deletions(-) create mode 100644 tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs diff --git a/src/LaunchDarkly.Xamarin/FeatureFlag.cs b/src/LaunchDarkly.Xamarin/FeatureFlag.cs index 7d847c02..bd37ecbf 100644 --- a/src/LaunchDarkly.Xamarin/FeatureFlag.cs +++ b/src/LaunchDarkly.Xamarin/FeatureFlag.cs @@ -1,25 +1,27 @@ using System; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using LaunchDarkly.Common; - -namespace LaunchDarkly.Xamarin -{ - public class FeatureFlag : IEquatable - { - public JToken value; - public int version; - public bool trackEvents; - public int? variation; - public long? debugEventsUntilDate; - - public bool Equals(FeatureFlag otherFlag) - { + +namespace LaunchDarkly.Xamarin +{ + public class FeatureFlag : IEquatable + { + public JToken value; + public int version; + public int? flagVersion; + public bool trackEvents; + public int? variation; + public long? debugEventsUntilDate; + + public bool Equals(FeatureFlag otherFlag) + { return JToken.DeepEquals(value, otherFlag.value) && version == otherFlag.version + && flagVersion == otherFlag.flagVersion && trackEvents == otherFlag.trackEvents && variation == otherFlag.variation - && debugEventsUntilDate == otherFlag.debugEventsUntilDate; - } + && debugEventsUntilDate == otherFlag.debugEventsUntilDate; + } } internal class FeatureFlagEvent : IFlagEventProperties @@ -39,10 +41,10 @@ public FeatureFlagEvent(string key, FeatureFlag featureFlag) _featureFlag = featureFlag; _key = key; } - - public string Key => _key; - public int Version => _featureFlag.version; - public bool TrackEvents => _featureFlag.trackEvents; - public long? DebugEventsUntilDate => _featureFlag.debugEventsUntilDate; - } -} + + public string Key => _key; + public int Version => _featureFlag.flagVersion ?? _featureFlag.version; + public bool TrackEvents => _featureFlag.trackEvents; + public long? DebugEventsUntilDate => _featureFlag.debugEventsUntilDate; + } +} diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 1c8261f8..d7824bdc 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -67,8 +67,8 @@ public sealed class LdClient : ILdMobileClient deviceInfo = Factory.CreateDeviceInfo(configuration); flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); - // If you pass in a null user or user with an empty key, one will be assigned to them. - if (user == null || String.IsNullOrEmpty(user.Key)) + // If you pass in a null user or user with a null key, one will be assigned to them. + if (user == null || user.Key == null) { User = UserWithUniqueKey(user); } @@ -427,7 +427,7 @@ public async Task IdentifyAsync(User user) } User userWithKey = null; - if (String.IsNullOrEmpty(user.Key)) + if (user.Key == null) { userWithKey = UserWithUniqueKey(user); } @@ -503,11 +503,11 @@ void IDisposable.Dispose() void Dispose(bool disposing) { - if (disposing) + if (disposing) { - Log.InfoFormat("The mobile client is being disposed"); + Log.InfoFormat("The mobile client is being disposed"); updateProcessor.Dispose(); - eventProcessor.Dispose(); + eventProcessor.Dispose(); } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs new file mode 100644 index 00000000..bd990c54 --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagTests.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class FeatureFlagEventTests + { + [Fact] + public void ReturnsFlagVersionAsVersion() + { + var flag = new FeatureFlag(); + flag.flagVersion = 123; + flag.version = 456; + var flagEvent = new FeatureFlagEvent("my-flag", flag); + Assert.Equal(123, flagEvent.Version); + } + + [Fact] + public void FallsBackToVersionAsVersion() + { + var flag = new FeatureFlag(); + flag.version = 456; + var flagEvent = new FeatureFlagEvent("my-flag", flag); + Assert.Equal(456, flagEvent.Version); + } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs b/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs index c326ceed..3b0df506 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs @@ -1,106 +1,106 @@ -using System; -using LaunchDarkly.Client; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace LaunchDarkly.Xamarin.Tests -{ - public class FlagCacheManagerTests - { - IUserFlagCache deviceCache = new UserFlagInMemoryCache(); - IUserFlagCache inMemoryCache = new UserFlagInMemoryCache(); - FeatureFlagListenerManager listenerManager = new FeatureFlagListenerManager(); - - User user = User.WithKey("someKey"); - - IFlagCacheManager ManagerWithCachedFlags() - { - var flagCacheManager = new FlagCacheManager(deviceCache, inMemoryCache, listenerManager, user); - flagCacheManager.CacheFlagsFromService(JSONReader.StubbedFlagsDictionary(), user); - return flagCacheManager; - } - - [Fact] - public void CacheFlagsShouldStoreFlagsInDeviceCache() - { - var flagCacheManager = ManagerWithCachedFlags(); - var cachedDeviceFlags = deviceCache.RetrieveFlags(user); - Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); - Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); - Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); - } - - [Fact] - public void CacheFlagsShouldAlsoStoreFlagsInMemoryCache() - { - var flagCacheManager = ManagerWithCachedFlags(); - var cachedDeviceFlags = inMemoryCache.RetrieveFlags(user); - Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); - Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); - Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); - } - - [Fact] - public void ShouldBeAbleToRemoveFlagForUser() - { - var manager = ManagerWithCachedFlags(); - manager.RemoveFlagForUser("int-key", user); - Assert.Null(manager.FlagForUser("int-key", user)); - } - - [Fact] - public void ShouldBeAbleToUpdateFlagForUser() - { - var flagCacheManager = ManagerWithCachedFlags(); - var updatedFeatureFlag = new FeatureFlag(); - updatedFeatureFlag.value = JToken.FromObject(5); - updatedFeatureFlag.version = 12; - flagCacheManager.UpdateFlagForUser("int-flag", updatedFeatureFlag, user); - var updatedFlagFromCache = flagCacheManager.FlagForUser("int-flag", user); - Assert.Equal(5, updatedFlagFromCache.value.ToObject()); - Assert.Equal(12, updatedFeatureFlag.version); - } - - [Fact] - public void UpdateFlagUpdatesTheFlagOnListenerManager() - { - var listener = new TestListener(); - listenerManager.RegisterListener(listener, "int-flag"); - var flagCacheManager = ManagerWithCachedFlags(); - var updatedFeatureFlag = new FeatureFlag(); - updatedFeatureFlag.value = JToken.FromObject(7); - flagCacheManager.UpdateFlagForUser("int-flag", updatedFeatureFlag, user); - - Assert.Equal(7, listener.FeatureFlags["int-flag"].ToObject()); - } - - [Fact] - public void RemoveFlagTellsListenerManagerToTellListenersFlagWasDeleted() - { - var listener = new TestListener(); - listenerManager.RegisterListener(listener, "int-flag"); - listener.FeatureFlags["int-flag"] = JToken.FromObject(1); - Assert.True(listener.FeatureFlags.ContainsKey("int-flag")); - - var flagCacheManager = ManagerWithCachedFlags(); - var updatedFeatureFlag = new FeatureFlag(); - updatedFeatureFlag.value = JToken.FromObject(7); - flagCacheManager.RemoveFlagForUser("int-flag", user); - - Assert.False(listener.FeatureFlags.ContainsKey("int-flag")); - } - - [Fact] - public void CacheFlagsFromServiceUpdatesListenersIfFlagValueChanged() - { - var flagCacheManager = ManagerWithCachedFlags(); - var listener = new TestListener(); - listenerManager.RegisterListener(listener, "int-flag"); - - var updatedFlags = JSONReader.UpdatedStubbedFlagsDictionary(); - flagCacheManager.CacheFlagsFromService(updatedFlags, user); - - Assert.Equal(5, listener.FeatureFlags["int-flag"].ToObject()); - } - } -} +using System; +using LaunchDarkly.Client; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class FlagCacheManagerTests + { + IUserFlagCache deviceCache = new UserFlagInMemoryCache(); + IUserFlagCache inMemoryCache = new UserFlagInMemoryCache(); + FeatureFlagListenerManager listenerManager = new FeatureFlagListenerManager(); + + User user = User.WithKey("someKey"); + + IFlagCacheManager ManagerWithCachedFlags() + { + var flagCacheManager = new FlagCacheManager(deviceCache, inMemoryCache, listenerManager, user); + flagCacheManager.CacheFlagsFromService(JSONReader.StubbedFlagsDictionary(), user); + return flagCacheManager; + } + + [Fact] + public void CacheFlagsShouldStoreFlagsInDeviceCache() + { + var flagCacheManager = ManagerWithCachedFlags(); + var cachedDeviceFlags = deviceCache.RetrieveFlags(user); + Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); + Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); + Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); + } + + [Fact] + public void CacheFlagsShouldAlsoStoreFlagsInMemoryCache() + { + var flagCacheManager = ManagerWithCachedFlags(); + var cachedDeviceFlags = inMemoryCache.RetrieveFlags(user); + Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); + Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); + Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); + } + + [Fact] + public void ShouldBeAbleToRemoveFlagForUser() + { + var manager = ManagerWithCachedFlags(); + manager.RemoveFlagForUser("int-key", user); + Assert.Null(manager.FlagForUser("int-key", user)); + } + + [Fact] + public void ShouldBeAbleToUpdateFlagForUser() + { + var flagCacheManager = ManagerWithCachedFlags(); + var updatedFeatureFlag = new FeatureFlag(); + updatedFeatureFlag.value = JToken.FromObject(5); + updatedFeatureFlag.version = 12; + flagCacheManager.UpdateFlagForUser("int-flag", updatedFeatureFlag, user); + var updatedFlagFromCache = flagCacheManager.FlagForUser("int-flag", user); + Assert.Equal(5, updatedFlagFromCache.value.ToObject()); + Assert.Equal(12, updatedFeatureFlag.version); + } + + [Fact] + public void UpdateFlagUpdatesTheFlagOnListenerManager() + { + var listener = new TestListener(); + listenerManager.RegisterListener(listener, "int-flag"); + var flagCacheManager = ManagerWithCachedFlags(); + var updatedFeatureFlag = new FeatureFlag(); + updatedFeatureFlag.value = JToken.FromObject(7); + flagCacheManager.UpdateFlagForUser("int-flag", updatedFeatureFlag, user); + + Assert.Equal(7, listener.FeatureFlags["int-flag"].ToObject()); + } + + [Fact] + public void RemoveFlagTellsListenerManagerToTellListenersFlagWasDeleted() + { + var listener = new TestListener(); + listenerManager.RegisterListener(listener, "int-flag"); + listener.FeatureFlags["int-flag"] = JToken.FromObject(1); + Assert.True(listener.FeatureFlags.ContainsKey("int-flag")); + + var flagCacheManager = ManagerWithCachedFlags(); + var updatedFeatureFlag = new FeatureFlag(); + updatedFeatureFlag.value = JToken.FromObject(7); + flagCacheManager.RemoveFlagForUser("int-flag", user); + + Assert.False(listener.FeatureFlags.ContainsKey("int-flag")); + } + + [Fact] + public void CacheFlagsFromServiceUpdatesListenersIfFlagValueChanged() + { + var flagCacheManager = ManagerWithCachedFlags(); + var listener = new TestListener(); + listenerManager.RegisterListener(listener, "int-flag"); + + var updatedFlags = JSONReader.UpdatedStubbedFlagsDictionary(); + flagCacheManager.CacheFlagsFromService(updatedFlags, user); + + Assert.Equal(5, listener.FeatureFlags["int-flag"].ToObject()); + } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 37c493cb..be383811 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -1,61 +1,61 @@ using System; -using System.Collections.Generic; -using LaunchDarkly.Client; -using Newtonsoft.Json; +using System.Collections.Generic; +using LaunchDarkly.Client; +using Newtonsoft.Json; using Xunit; namespace LaunchDarkly.Xamarin.Tests { public class DefaultLdClientTests - { - static readonly string appKey = "some app key"; - static readonly string flagKey = "some flag key"; - - LdClient Client() + { + static readonly string appKey = "some app key"; + static readonly string flagKey = "some flag key"; + + LdClient Client() { if (LdClient.Instance == null) { User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var configuration = StubbedConfigAndUserBuilder.Config(user, appKey); + var configuration = StubbedConfigAndUserBuilder.Config(user, appKey); return LdClient.Init(configuration, user); } - return LdClient.Instance; - } - + return LdClient.Instance; + } + [Fact] - public void CanCreateClientWithConfigAndUser() - { + public void CanCreateClientWithConfigAndUser() + { Assert.NotNull(Client()); } [Fact] - public void DefaultBoolVariationFlag() - { + public void DefaultBoolVariationFlag() + { Assert.False(Client().BoolVariation(flagKey)); } [Fact] public void DefaultStringVariationFlag() - { - Assert.Equal(String.Empty, Client().StringVariation(flagKey, String.Empty)); + { + Assert.Equal(String.Empty, Client().StringVariation(flagKey, String.Empty)); } [Fact] public void DefaultFloatVariationFlag() - { + { Assert.Equal(0, Client().FloatVariation(flagKey)); } [Fact] public void DefaultIntVariationFlag() - { + { Assert.Equal(0, Client().IntVariation(flagKey)); } [Fact] public void DefaultJSONVariationFlag() - { + { Assert.Null(Client().JsonVariation(flagKey, null)); } @@ -63,7 +63,7 @@ public void DefaultJSONVariationFlag() public void DefaultAllFlagsShouldBeEmpty() { var client = Client(); - client.Identify(User.WithKey("some other user key with no flags")); + client.Identify(User.WithKey("some other user key with no flags")); Assert.Equal(0, client.AllFlags().Count); client.Identify(User.WithKey("user1Key")); } @@ -74,23 +74,23 @@ public void DefaultValueReturnedIfTypeBackIsDifferent() var client = Client(); Assert.Equal(0, client.IntVariation("string-flag", 0)); Assert.False(client.BoolVariation("float-flag", false)); - } - + } + [Fact] - public void IdentifyUpdatesTheUser() + public void IdentifyUpdatesTheUser() { - var client = Client(); - var updatedUser = User.WithKey("some new key"); - client.Identify(updatedUser); - Assert.Equal(client.User, updatedUser); + var client = Client(); + var updatedUser = User.WithKey("some new key"); + client.Identify(updatedUser); + Assert.Equal(client.User, updatedUser); } [Fact] - public void SharedClientIsTheOnlyClientAvailable() + public void SharedClientIsTheOnlyClientAvailable() { - var client = Client(); - var config = Configuration.Default(appKey); - Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); + var client = Client(); + var config = Configuration.Default(appKey); + Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); } [Fact] @@ -112,27 +112,27 @@ public void ConnectionManagerShouldKnowIfOnlineOrNot() connMgr.Connect(true); Assert.False(client.IsOffline()); connMgr.Connect(false); - Assert.False(client.Online); + Assert.False(client.Online); } [Fact] public void ConnectionChangeShouldStopUpdateProcessor() - { + { var client = Client(); var connMgr = client.Config.ConnectionManager as MockConnectionManager; connMgr.ConnectionChanged += (bool obj) => client.Online = obj; connMgr.Connect(false); var mockUpdateProc = client.Config.MobileUpdateProcessor as MockPollingProcessor; - Assert.False(mockUpdateProc.IsRunning); + Assert.False(mockUpdateProc.IsRunning); } [Fact] - public void UserWithMissingKeyWillHaveUniqueKeySet() + public void UserWithNullKeyWillHaveUniqueKeySet() { LdClient.Instance = null; - var userWithoutKey = User.WithKey(String.Empty); - var config = StubbedConfigAndUserBuilder.Config(userWithoutKey, "someOtherAppKey"); - var client = LdClient.Init(config, userWithoutKey); + var userWithNullKey = User.WithKey(null); + var config = StubbedConfigAndUserBuilder.Config(userWithNullKey, "someOtherAppKey"); + var client = LdClient.Init(config, userWithNullKey); Assert.Equal(MockDeviceInfo.key, client.User.Key); LdClient.Instance = null; } @@ -140,8 +140,10 @@ public void UserWithMissingKeyWillHaveUniqueKeySet() [Fact] public void IdentifyWithUserMissingKeyUsesUniqueGeneratedKey() { + var client = Client(); LdClient.Instance.Identify(User.WithKey("a new user's key")); - LdClient.Instance.Identify(User.WithKey(String.Empty)); + var userWithNullKey = User.WithKey(null); + LdClient.Instance.Identify(userWithNullKey); Assert.Equal(MockDeviceInfo.key, LdClient.Instance.User.Key); LdClient.Instance = null; } @@ -200,6 +202,6 @@ public void UnregisterListenerUnregistersPassedInListenerForFlagKeyOnListenerMan client.UnregisterFeatureFlagListener("user2-flag", listener); listenerMgr.FlagWasUpdated("user2-flag", 12); Assert.NotEqual(12, listener.FeatureFlags["user2-flag"]); - } + } } } From 56165a0dac1298df45e55c4b728557e05b366b96 Mon Sep 17 00:00:00 2001 From: Andrew Shannon Brown Date: Thu, 12 Jul 2018 21:00:38 -0700 Subject: [PATCH 03/47] Send default feature flag event when flag.value is null (#6) Also return default value instead of null value. --- src/LaunchDarkly.Xamarin/LdClient.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index d7824bdc..189202b8 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -334,13 +334,21 @@ JToken Variation(string featureKey, JToken defaultValue) if (flag != null) { featureFlagEvent = new FeatureFlagEvent(featureKey, flag); - featureRequestEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, - User, - flag.variation, - flag.value, - defaultValue); + var value = flag.value; + if (value == null) { + featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, + User, + defaultValue); + value = defaultValue; + } else { + featureRequestEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, + User, + flag.variation, + flag.value, + defaultValue); + } eventProcessor.SendEvent(featureRequestEvent); - return flag.value; + return value; } Log.InfoFormat("Unknown feature flag {0}; returning default value", From d0ca44e9ba7d3ea747bf988fef82513c64507abc Mon Sep 17 00:00:00 2001 From: Andrew Shannon Brown Date: Fri, 13 Jul 2018 07:15:51 -0700 Subject: [PATCH 04/47] Create License.txt --- License.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 License.txt diff --git a/License.txt b/License.txt new file mode 100644 index 00000000..f8503553 --- /dev/null +++ b/License.txt @@ -0,0 +1,13 @@ +Copyright 2018 Catamorphic, Co. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. From 2644d7f5e063e91ee52471af200a8a0cb66fbac9 Mon Sep 17 00:00:00 2001 From: Andrew Shannon Brown Date: Fri, 13 Jul 2018 15:10:02 -0700 Subject: [PATCH 05/47] Add test that we return default value for off variation (#7) --- .../FeatureFlagsFromService.json | 5 +++++ tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs | 7 +++++++ .../MobilePollingProcessorTests.cs | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json index cb9f23d4..37a9b008 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json +++ b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json @@ -29,5 +29,10 @@ "version": 456, "variation": 1, "trackEvents": false + }, + "off-flag": { + "value": null, + "version": 456, + "trackEvents": false } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index be383811..25f9f7cf 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -76,6 +76,13 @@ public void DefaultValueReturnedIfTypeBackIsDifferent() Assert.False(client.BoolVariation("float-flag", false)); } + [Fact] + public void DefaultValueReturnedIfFlagIsOff() + { + var client = Client(); + Assert.Equal(123, client.IntVariation("off-flag", 123)); + } + [Fact] public void IdentifyUpdatesTheUser() { diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs b/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs index a946fd5f..3a9552ce 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs @@ -32,7 +32,7 @@ public void StartWaitsUntilFlagCacheFilled() var initTask = processor.Start(); var unused = initTask.Wait(TimeSpan.FromSeconds(1)); var flags = mockFlagCacheManager.FlagsForUser(user); - Assert.Equal(5, flags.Count); + Assert.Equal(6, flags.Count); } } } From 7fdf036084335bb3b7a7f01fe866e66d6ed619f9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 13 Jul 2018 15:39:59 -0700 Subject: [PATCH 06/47] misc cleanup of project files --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 7 ------- .../LaunchDarkly.Xamarin.Tests.csproj | 5 ----- 2 files changed, 12 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 2adb9c96..2f398394 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -4,7 +4,6 @@ netstandard1.6;netstandard2.0;net45 true ..\..\LaunchDarkly.Xamarin.snk - netstandard2.0 @@ -24,12 +23,6 @@ - - - - - - diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index 33b64dce..a4a01263 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -27,11 +27,6 @@ - - - - - Always From 8924b5cb7ce7347d03666eef2bb816d249c88980 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 13 Jul 2018 15:40:22 -0700 Subject: [PATCH 07/47] rm usage that won't work in older target frameworks --- src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs b/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs index 40bdc3a2..1cd8c9b9 100644 --- a/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs +++ b/src/LaunchDarkly.Xamarin/MobileStreamingProcessor.cs @@ -129,7 +129,7 @@ Task IStreamProcessor.HandleMessage(StreamManager streamManager, string messageT break; } - return Task.CompletedTask; + return Task.FromResult(true); } void PatchFeatureFlag(string flagKey, FeatureFlag featureFlag) From b3287b691aa74423c65abc3c22c312b609be60fa Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 13 Jul 2018 15:54:26 -0700 Subject: [PATCH 08/47] version 1.0.0-beta8 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 2f398394..2d110ff1 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,6 +1,7 @@ + 1.0.0-beta8 netstandard1.6;netstandard2.0;net45 true ..\..\LaunchDarkly.Xamarin.snk From dcc7f9fe007546862578ccf2ddb6696b337231d9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 13 Jul 2018 16:01:23 -0700 Subject: [PATCH 09/47] version 1.0.0-beta9 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 2d110ff1..702070f3 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,7 +1,7 @@ - 1.0.0-beta8 + 1.0.0-beta9 netstandard1.6;netstandard2.0;net45 true ..\..\LaunchDarkly.Xamarin.snk From c2f674a49f67482d139de5ce0b582559d49b43c5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 17 Jul 2018 16:53:38 -0700 Subject: [PATCH 10/47] clean up unnecessary static references and make tests stable --- src/LaunchDarkly.Xamarin/LdClient.cs | 25 ++++++------ .../LdClientTests.cs | 39 ++++++++++--------- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 27 +++++++++++++ 3 files changed, 62 insertions(+), 29 deletions(-) create mode 100644 tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 189202b8..c404b742 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -148,7 +148,7 @@ public static LdClient Init(Configuration config, User user) if (Instance.Online) { - StartUpdateProcessor(); + Instance.StartUpdateProcessor(); } return Instance; @@ -173,7 +173,7 @@ public static Task InitAsync(Configuration config, User user) if (Instance.Online) { - Task t = StartUpdateProcessorAsync(); + Task t = Instance.StartUpdateProcessorAsync(); return t.ContinueWith((result) => Instance); } else @@ -192,16 +192,15 @@ static void CreateInstance(Configuration configuration, User user) Instance.Version); } - static void StartUpdateProcessor() + void StartUpdateProcessor() { - var initTask = Instance.updateProcessor.Start(); - var configuration = Instance.Config as Configuration; - var unused = initTask.Wait(configuration.StartWaitTime); + var initTask = updateProcessor.Start(); + var unused = initTask.Wait(Config.StartWaitTime); } - static Task StartUpdateProcessorAsync() + Task StartUpdateProcessorAsync() { - return Instance.updateProcessor.Start(); + return updateProcessor.Start(); } void SetupConnectionManager() @@ -300,7 +299,7 @@ JToken VariationWithType(string featureKey, JToken defaultValue, JTokenType? jto { Log.ErrorFormat("Expected type: {0} but got {1} when evaluating FeatureFlag: {2}. Returning default", jtokenType, - returnedFlagValue.GetType(), + returnedFlagValue.Type, featureKey); return defaultValue; @@ -403,8 +402,12 @@ public void Track(string eventName) /// public bool Initialized() { - bool isInited = Instance != null; - return isInited && Online; + //bool isInited = Instance != null; + //return isInited && Online; + // TODO: This method needs to be fixed to actually check whether the update processor has initialized. + // The previous logic (above) was meaningless because this method is not static, so by definition you + // do have a client instance if we've gotten here. But that doesn't mean it is initialized. + return Online; } /// diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 25f9f7cf..c1b38d9d 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -13,14 +13,9 @@ public class DefaultLdClientTests LdClient Client() { - if (LdClient.Instance == null) - { - User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var configuration = StubbedConfigAndUserBuilder.Config(user, appKey); - return LdClient.Init(configuration, user); - } - - return LdClient.Instance; + User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); + var configuration = StubbedConfigAndUserBuilder.Config(user, appKey); + return TestUtil.CreateClient(configuration, user); } [Fact] @@ -95,9 +90,20 @@ public void IdentifyUpdatesTheUser() [Fact] public void SharedClientIsTheOnlyClientAvailable() { - var client = Client(); - var config = Configuration.Default(appKey); - Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); + lock (TestUtil.ClientInstanceLock) + { + User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); + var config = StubbedConfigAndUserBuilder.Config(user, appKey); + var client = LdClient.Init(config, user); + try + { + Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); + } + finally + { + LdClient.Instance = null; + } + } } [Fact] @@ -136,23 +142,20 @@ public void ConnectionChangeShouldStopUpdateProcessor() [Fact] public void UserWithNullKeyWillHaveUniqueKeySet() { - LdClient.Instance = null; var userWithNullKey = User.WithKey(null); var config = StubbedConfigAndUserBuilder.Config(userWithNullKey, "someOtherAppKey"); - var client = LdClient.Init(config, userWithNullKey); + var client = TestUtil.CreateClient(config, userWithNullKey); Assert.Equal(MockDeviceInfo.key, client.User.Key); - LdClient.Instance = null; } [Fact] public void IdentifyWithUserMissingKeyUsesUniqueGeneratedKey() { var client = Client(); - LdClient.Instance.Identify(User.WithKey("a new user's key")); + client.Identify(User.WithKey("a new user's key")); var userWithNullKey = User.WithKey(null); - LdClient.Instance.Identify(userWithNullKey); - Assert.Equal(MockDeviceInfo.key, LdClient.Instance.User.Key); - LdClient.Instance = null; + client.Identify(userWithNullKey); + Assert.Equal(MockDeviceInfo.key, client.User.Key); } [Fact] diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs new file mode 100644 index 00000000..8624f1b0 --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using LaunchDarkly.Client; + +namespace LaunchDarkly.Xamarin.Tests +{ + class TestUtil + { + // Any tests that are going to access the static LdClient.Instance must hold this lock, + // to avoid interfering with tests that use CreateClient. + public static readonly object ClientInstanceLock = new object(); + + // Calls LdClient.Init, but then sets LdClient.Instance to null so other tests can + // instantiate their own independent clients. Application code cannot do this because + // the LdClient.Instance setter has internal scope. + public static LdClient CreateClient(Configuration config, User user) + { + lock (ClientInstanceLock) + { + LdClient client = LdClient.Init(config, user); + LdClient.Instance = null; + return client; + } + } + } +} From 27c0a396b28454a55910e1cd161b7ca74286dcfe Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 17 Jul 2018 17:37:30 -0700 Subject: [PATCH 11/47] break out and simplify basic flag evaluation tests --- .../LdClientEvaluationTests.cs | 137 ++++++++++++++++++ .../LdClientTests.cs | 75 +--------- .../StubbedConfigAndUserBuilder.cs | 22 --- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 39 ++++- 4 files changed, 177 insertions(+), 96 deletions(-) create mode 100644 tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs new file mode 100644 index 00000000..78d9fe9a --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientEvaluationTests.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Text; +using LaunchDarkly.Client; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class LdClientEvaluationTests + { + static readonly string appKey = "some app key"; + static readonly string nonexistentFlagKey = "some flag key"; + static readonly User user = User.WithKey("userkey"); + + private static LdClient ClientWithFlagsJson(string flagsJson) + { + var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); + return TestUtil.CreateClient(config, user); + } + + [Fact] + public void BoolVariationReturnsValue() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(true)); + var client = ClientWithFlagsJson(flagsJson); + + Assert.True(client.BoolVariation("flag-key", false)); + } + + [Fact] + public void BoolVariationReturnsDefaultForUnknownFlag() + { + var client = ClientWithFlagsJson("{}"); + Assert.False(client.BoolVariation(nonexistentFlagKey)); + } + + [Fact] + public void IntVariationReturnsValue() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(3)); + var client = ClientWithFlagsJson(flagsJson); + + Assert.Equal(3, client.IntVariation("flag-key", 0)); + } + + [Fact] + public void IntVariationReturnsDefaultForUnknownFlag() + { + var client = ClientWithFlagsJson("{}"); + Assert.Equal(1, client.IntVariation(nonexistentFlagKey, 1)); + } + + [Fact] + public void FloatVariationReturnsValue() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue(2.5f)); + var client = ClientWithFlagsJson(flagsJson); + + Assert.Equal(2.5f, client.FloatVariation("flag-key", 0)); + } + + [Fact] + public void FloatVariationReturnsDefaultForUnknownFlag() + { + var client = ClientWithFlagsJson("{}"); + Assert.Equal(0.5f, client.FloatVariation(nonexistentFlagKey, 0.5f)); + } + + [Fact] + public void StringVariationReturnsValue() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue("string value")); + var client = ClientWithFlagsJson(flagsJson); + + Assert.Equal("string value", client.StringVariation("flag-key", "")); + } + + [Fact] + public void StringVariationReturnsDefaultForUnknownFlag() + { + var client = ClientWithFlagsJson("{}"); + Assert.Equal("d", client.StringVariation(nonexistentFlagKey, "d")); + } + + [Fact] + public void JsonVariationReturnsValue() + { + var jsonValue = new JObject { { "thing", new JValue("stuff") } }; + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", jsonValue); + var client = ClientWithFlagsJson(flagsJson); + + var defaultValue = new JValue(3); + Assert.Equal(jsonValue, client.JsonVariation("flag-key", defaultValue)); + } + + [Fact] + public void JsonVariationReturnsDefaultForUnknownFlag() + { + var client = ClientWithFlagsJson("{}"); + Assert.Null(client.JsonVariation(nonexistentFlagKey, null)); + } + + [Fact] + public void AllFlagsReturnsAllFlagValues() + { + var flagsJson = @"{""flag1"":{""value"":""a""},""flag2"":{""value"":""b""}}"; + var client = ClientWithFlagsJson(flagsJson); + + var result = client.AllFlags(); + Assert.Equal(2, result.Count); + Assert.Equal(new JValue("a"), result["flag1"]); + Assert.Equal(new JValue("b"), result["flag2"]); + } + + [Fact] + public void DefaultValueReturnedIfValueTypeIsDifferent() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", new JValue("string value")); + var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); + var client = TestUtil.CreateClient(config, user); + + Assert.Equal(3, client.IntVariation("flag-key", 3)); + } + + [Fact] + public void DefaultValueReturnedIfFlagValueIsNull() + { + string flagsJson = TestUtil.JsonFlagsWithSingleFlag("flag-key", null); + var config = TestUtil.ConfigWithFlagsJson(user, appKey, flagsJson); + var client = TestUtil.CreateClient(config, user); + + Assert.Equal(3, client.IntVariation("flag-key", 3)); + } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index c1b38d9d..41b2794d 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using LaunchDarkly.Client; -using Newtonsoft.Json; using Xunit; namespace LaunchDarkly.Xamarin.Tests @@ -9,12 +7,11 @@ namespace LaunchDarkly.Xamarin.Tests public class DefaultLdClientTests { static readonly string appKey = "some app key"; - static readonly string flagKey = "some flag key"; LdClient Client() { User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var configuration = StubbedConfigAndUserBuilder.Config(user, appKey); + var configuration = TestUtil.ConfigWithFlagsJson(user, appKey, JSONReader.FeatureFlagJSON()); return TestUtil.CreateClient(configuration, user); } @@ -24,60 +21,6 @@ public void CanCreateClientWithConfigAndUser() Assert.NotNull(Client()); } - [Fact] - public void DefaultBoolVariationFlag() - { - Assert.False(Client().BoolVariation(flagKey)); - } - - [Fact] - public void DefaultStringVariationFlag() - { - Assert.Equal(String.Empty, Client().StringVariation(flagKey, String.Empty)); - } - - [Fact] - public void DefaultFloatVariationFlag() - { - Assert.Equal(0, Client().FloatVariation(flagKey)); - } - - [Fact] - public void DefaultIntVariationFlag() - { - Assert.Equal(0, Client().IntVariation(flagKey)); - } - - [Fact] - public void DefaultJSONVariationFlag() - { - Assert.Null(Client().JsonVariation(flagKey, null)); - } - - [Fact] - public void DefaultAllFlagsShouldBeEmpty() - { - var client = Client(); - client.Identify(User.WithKey("some other user key with no flags")); - Assert.Equal(0, client.AllFlags().Count); - client.Identify(User.WithKey("user1Key")); - } - - [Fact] - public void DefaultValueReturnedIfTypeBackIsDifferent() - { - var client = Client(); - Assert.Equal(0, client.IntVariation("string-flag", 0)); - Assert.False(client.BoolVariation("float-flag", false)); - } - - [Fact] - public void DefaultValueReturnedIfFlagIsOff() - { - var client = Client(); - Assert.Equal(123, client.IntVariation("off-flag", 123)); - } - [Fact] public void IdentifyUpdatesTheUser() { @@ -93,7 +36,7 @@ public void SharedClientIsTheOnlyClientAvailable() lock (TestUtil.ClientInstanceLock) { User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var config = StubbedConfigAndUserBuilder.Config(user, appKey); + var config = TestUtil.ConfigWithFlagsJson(user, appKey, "{}"); var client = LdClient.Init(config, user); try { @@ -105,17 +48,7 @@ public void SharedClientIsTheOnlyClientAvailable() } } } - - [Fact] - public void CanFetchFlagFromInMemoryCache() - { - var client = Client(); - bool boolFlag = client.BoolVariation("boolean-flag", true); - Assert.True(boolFlag); - int intFlag = client.IntVariation("int-flag", 0); - Assert.Equal(15, intFlag); - } - + [Fact] public void ConnectionManagerShouldKnowIfOnlineOrNot() { @@ -143,7 +76,7 @@ public void ConnectionChangeShouldStopUpdateProcessor() public void UserWithNullKeyWillHaveUniqueKeySet() { var userWithNullKey = User.WithKey(null); - var config = StubbedConfigAndUserBuilder.Config(userWithNullKey, "someOtherAppKey"); + var config = TestUtil.ConfigWithFlagsJson(userWithNullKey, "someOtherAppKey", "{}"); var client = TestUtil.CreateClient(config, userWithNullKey); Assert.Equal(MockDeviceInfo.key, client.User.Key); } diff --git a/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs b/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs index 3e5a2beb..5622e9d3 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs @@ -7,28 +7,6 @@ namespace LaunchDarkly.Xamarin.Tests { public static class StubbedConfigAndUserBuilder { - public static Configuration Config(User user, string appKey) - { - var stubbedFlagCache = JSONReader.StubbedFlagCache(user); - - // overriding the default implementation of dependencies for testing purposes - var mockOnlineConnectionManager = new MockConnectionManager(true); - var mockFlagCacheManager = new MockFlagCacheManager(stubbedFlagCache); - var mockPollingProcessor = new MockPollingProcessor(); - var mockPersister = new MockPersister(); - var mockDeviceInfo = new MockDeviceInfo(); - var featureFlagListener = new FeatureFlagListenerManager(); - - Configuration configuration = Configuration.Default(appKey) - .WithFlagCacheManager(mockFlagCacheManager) - .WithConnectionManager(mockOnlineConnectionManager) - .WithUpdateProcessor(mockPollingProcessor) - .WithPersister(mockPersister) - .WithDeviceInfo(mockDeviceInfo) - .WithFeatureFlagListenerManager(featureFlagListener); - return configuration; - } - public static User UserWithAllPropertiesFilledIn(string key) { var user = User.WithKey(key); diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 8624f1b0..3db8735e 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; using LaunchDarkly.Client; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace LaunchDarkly.Xamarin.Tests { @@ -23,5 +23,38 @@ public static LdClient CreateClient(Configuration config, User user) return client; } } + + public static string JsonFlagsWithSingleFlag(string flagKey, JToken value) + { + JObject fo = new JObject { { "value", value } }; + JObject o = new JObject { { flagKey, fo } }; + return JsonConvert.SerializeObject(o); + } + + public static Configuration ConfigWithFlagsJson(User user, string appKey, string flagsJson) + { + var flags = JsonConvert.DeserializeObject>(flagsJson); + IUserFlagCache stubbedFlagCache = new UserFlagInMemoryCache(); + if (user != null && user.Key != null) + { + stubbedFlagCache.CacheFlagsForUser(flags, user); + } + + var mockOnlineConnectionManager = new MockConnectionManager(true); + var mockFlagCacheManager = new MockFlagCacheManager(stubbedFlagCache); + var mockPollingProcessor = new MockPollingProcessor(); + var mockPersister = new MockPersister(); + var mockDeviceInfo = new MockDeviceInfo(); + var featureFlagListener = new FeatureFlagListenerManager(); + + Configuration configuration = Configuration.Default(appKey) + .WithFlagCacheManager(mockFlagCacheManager) + .WithConnectionManager(mockOnlineConnectionManager) + .WithUpdateProcessor(mockPollingProcessor) + .WithPersister(mockPersister) + .WithDeviceInfo(mockDeviceInfo) + .WithFeatureFlagListenerManager(featureFlagListener); + return configuration; + } } } From e4f61094487e439064a30e0cf50c8bd02479f4c6 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 17 Jul 2018 18:06:07 -0700 Subject: [PATCH 12/47] get rid of test fixture files --- .../FeatureFlag.json | 33 ------------ .../FeatureFlagsFromService.json | 38 -------------- .../FlagCacheManagerTests.cs | 20 +++++--- .../LaunchDarkly.Xamarin.Tests/JSONReader.cs | 50 ------------------- .../LaunchDarkly.Xamarin.Tests.csproj | 8 --- .../LdClientTests.cs | 2 +- .../MobilePollingProcessorTests.cs | 10 +++- .../MobileStreamingProcessorTests.cs | 9 +++- .../MockFeatureFlagRequestor.cs | 10 +++- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 7 ++- .../UserFlagCacheTests.cs | 17 +++---- 11 files changed, 49 insertions(+), 155 deletions(-) delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/FeatureFlag.json delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/JSONReader.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlag.json b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlag.json deleted file mode 100644 index 0363fa5a..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlag.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "boolean-flag": { - "value": true, - "version": 123, - "variation": 0, - "trackEvents": true, - "debugEventsUntilDate": 1525196668210 - }, - "json-flag": { - "value": {"some-key": "some-value"}, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "float-flag": { - "value": 3.14159, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "int-flag": { - "value": 5, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "string-flag": { - "value": "string value", - "version": 456, - "variation": 1, - "trackEvents": false - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json b/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json deleted file mode 100644 index 37a9b008..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/FeatureFlagsFromService.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "boolean-flag": { - "value": true, - "version": 123, - "variation": 0, - "trackEvents": true, - "debugEventsUntilDate": 1525196668210 - }, - "json-flag": { - "value": {"some-key": "a new value"}, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "float-flag": { - "value": 13.14159, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "int-flag": { - "value": 15, - "version": 456, - "variation": 1, - "trackEvents": false - }, - "string-flag": { - "value": "markw@magenic.com", - "version": 456, - "variation": 1, - "trackEvents": false - }, - "off-flag": { - "value": null, - "version": 456, - "trackEvents": false - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs b/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs index 3b0df506..574cbfd0 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/FlagCacheManagerTests.cs @@ -1,5 +1,4 @@ -using System; -using LaunchDarkly.Client; +using LaunchDarkly.Client; using Newtonsoft.Json.Linq; using Xunit; @@ -7,6 +6,12 @@ namespace LaunchDarkly.Xamarin.Tests { public class FlagCacheManagerTests { + private const string initialFlagsJson = "{" + + "\"int-flag\":{\"value\":15}," + + "\"float-flag\":{\"value\":13.5}," + + "\"string-flag\":{\"value\":\"markw@magenic.com\"}" + + "}"; + IUserFlagCache deviceCache = new UserFlagInMemoryCache(); IUserFlagCache inMemoryCache = new UserFlagInMemoryCache(); FeatureFlagListenerManager listenerManager = new FeatureFlagListenerManager(); @@ -16,7 +21,8 @@ public class FlagCacheManagerTests IFlagCacheManager ManagerWithCachedFlags() { var flagCacheManager = new FlagCacheManager(deviceCache, inMemoryCache, listenerManager, user); - flagCacheManager.CacheFlagsFromService(JSONReader.StubbedFlagsDictionary(), user); + var flags = TestUtil.DecodeFlagsJson(initialFlagsJson); + flagCacheManager.CacheFlagsFromService(flags, user); return flagCacheManager; } @@ -27,7 +33,7 @@ public void CacheFlagsShouldStoreFlagsInDeviceCache() var cachedDeviceFlags = deviceCache.RetrieveFlags(user); Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); - Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); + Assert.Equal(13.5, cachedDeviceFlags["float-flag"].value.ToObject()); } [Fact] @@ -37,7 +43,7 @@ public void CacheFlagsShouldAlsoStoreFlagsInMemoryCache() var cachedDeviceFlags = inMemoryCache.RetrieveFlags(user); Assert.Equal(15, cachedDeviceFlags["int-flag"].value.ToObject()); Assert.Equal("markw@magenic.com", cachedDeviceFlags["string-flag"].value.ToString()); - Assert.Equal(13.14159, cachedDeviceFlags["float-flag"].value.ToObject()); + Assert.Equal(13.5, cachedDeviceFlags["float-flag"].value.ToObject()); } [Fact] @@ -97,8 +103,8 @@ public void CacheFlagsFromServiceUpdatesListenersIfFlagValueChanged() var listener = new TestListener(); listenerManager.RegisterListener(listener, "int-flag"); - var updatedFlags = JSONReader.UpdatedStubbedFlagsDictionary(); - flagCacheManager.CacheFlagsFromService(updatedFlags, user); + var newFlagsJson = "{\"int-flag\":{\"value\":5}}"; + flagCacheManager.CacheFlagsFromService(TestUtil.DecodeFlagsJson(newFlagsJson), user); Assert.Equal(5, listener.FeatureFlags["int-flag"].ToObject()); } diff --git a/tests/LaunchDarkly.Xamarin.Tests/JSONReader.cs b/tests/LaunchDarkly.Xamarin.Tests/JSONReader.cs deleted file mode 100644 index 52469e89..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/JSONReader.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using LaunchDarkly.Client; -using Newtonsoft.Json; - -namespace LaunchDarkly.Xamarin.Tests -{ - public static class JSONReader - { - public static string FeatureFlagJSON() - { - return JSONTextFromFile("FeatureFlag.json"); - } - - public static string FeatureFlagJSONFromService() - { - return JSONTextFromFile("FeatureFlagsFromService.json"); - } - - public static string JSONTextFromFile(string filename) - { - return System.IO.File.ReadAllText(filename); - } - - internal static IUserFlagCache StubbedFlagCache(User user) - { - // stub json into the FlagCache - IUserFlagCache stubbedFlagCache = new UserFlagInMemoryCache(); - if (String.IsNullOrEmpty(user.Key)) - return stubbedFlagCache; - - stubbedFlagCache.CacheFlagsForUser(StubbedFlagsDictionary(), user); - return stubbedFlagCache; - } - - internal static IDictionary StubbedFlagsDictionary() - { - var text = FeatureFlagJSONFromService(); - var flags = JsonConvert.DeserializeObject>(text); - return flags; - } - - internal static IDictionary UpdatedStubbedFlagsDictionary() - { - var text = FeatureFlagJSON(); - var flags = JsonConvert.DeserializeObject>(text); - return flags; - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index a4a01263..903ade3f 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -27,12 +27,4 @@ - - - Always - - - Always - - diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 41b2794d..09990da8 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -11,7 +11,7 @@ public class DefaultLdClientTests LdClient Client() { User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var configuration = TestUtil.ConfigWithFlagsJson(user, appKey, JSONReader.FeatureFlagJSON()); + var configuration = TestUtil.ConfigWithFlagsJson(user, appKey, "{}"); return TestUtil.CreateClient(configuration, user); } diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs b/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs index 3a9552ce..1728fa34 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MobilePollingProcessorTests.cs @@ -6,12 +6,18 @@ namespace LaunchDarkly.Xamarin.Tests { public class MobilePollingProcessorTests { + private const string flagsJson = "{" + + "\"int-flag\":{\"value\":15}," + + "\"float-flag\":{\"value\":13.5}," + + "\"string-flag\":{\"value\":\"markw@magenic.com\"}" + + "}"; + IFlagCacheManager mockFlagCacheManager; User user; IMobileUpdateProcessor Processor() { - var mockFeatureFlagRequestor = new MockFeatureFlagRequestor(); + var mockFeatureFlagRequestor = new MockFeatureFlagRequestor(flagsJson); var stubbedFlagCache = new UserFlagInMemoryCache(); mockFlagCacheManager = new MockFlagCacheManager(stubbedFlagCache); user = User.WithKey("user1Key"); @@ -32,7 +38,7 @@ public void StartWaitsUntilFlagCacheFilled() var initTask = processor.Start(); var unused = initTask.Wait(TimeSpan.FromSeconds(1)); var flags = mockFlagCacheManager.FlagsForUser(user); - Assert.Equal(6, flags.Count); + Assert.Equal(3, flags.Count); } } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs index dccd107d..83f99c41 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs @@ -11,6 +11,12 @@ namespace LaunchDarkly.Xamarin.Tests { public class MobileStreamingProcessorTests { + private const string initialFlagsJson = "{" + + "\"int-flag\":{\"value\":15,\"version\":100}," + + "\"float-flag\":{\"value\":13.5,\"version\":100}," + + "\"string-flag\":{\"value\":\"markw@magenic.com\",\"version\":100}" + + "}"; + User user = User.WithKey("user key"); EventSourceMock mockEventSource; TestEventSourceFactory eventSourceFactory; @@ -150,8 +156,7 @@ string DeleteFlagWithLowerVersion() void PUTMessageSentToProcessor() { - string jsonData = JSONReader.FeatureFlagJSONFromService(); - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(jsonData, null), "put"); + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(initialFlagsJson, null), "put"); mockEventSource.RaiseMessageRcvd(eventArgs); } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs b/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs index 09cc07c6..59b6c964 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs @@ -4,6 +4,13 @@ namespace LaunchDarkly.Xamarin.Tests { internal class MockFeatureFlagRequestor : IFeatureFlagRequestor { + private readonly string _jsonFlags; + + public MockFeatureFlagRequestor(string jsonFlags) + { + _jsonFlags = jsonFlags; + } + public void Dispose() { @@ -11,8 +18,7 @@ public void Dispose() public Task FeatureFlagsAsync() { - var jsonText = JSONReader.FeatureFlagJSONFromService(); - var response = new WebResponse(200, jsonText, null); + var response = new WebResponse(200, _jsonFlags, null); return Task.FromResult(response); } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 3db8735e..25a3da41 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -31,9 +31,14 @@ public static string JsonFlagsWithSingleFlag(string flagKey, JToken value) return JsonConvert.SerializeObject(o); } + public static IDictionary DecodeFlagsJson(string flagsJson) + { + return JsonConvert.DeserializeObject>(flagsJson); + } + public static Configuration ConfigWithFlagsJson(User user, string appKey, string flagsJson) { - var flags = JsonConvert.DeserializeObject>(flagsJson); + var flags = DecodeFlagsJson(flagsJson); IUserFlagCache stubbedFlagCache = new UserFlagInMemoryCache(); if (user != null && user.Key != null) { diff --git a/tests/LaunchDarkly.Xamarin.Tests/UserFlagCacheTests.cs b/tests/LaunchDarkly.Xamarin.Tests/UserFlagCacheTests.cs index 86683063..4bfffcaa 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/UserFlagCacheTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/UserFlagCacheTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using LaunchDarkly.Client; -using Newtonsoft.Json; +using LaunchDarkly.Client; using Xunit; namespace LaunchDarkly.Xamarin.Tests @@ -16,14 +12,13 @@ public class UserFlagCacheTests [Fact] public void CanCacheFlagsInMemory() { - var text = JSONReader.FeatureFlagJSON(); - var flags = JsonConvert.DeserializeObject>(text); + var jsonFlags = @"{""flag1"":{""value"":1},""flag2"":{""value"":2}}"; + var flags = TestUtil.DecodeFlagsJson(jsonFlags); inMemoryCache.CacheFlagsForUser(flags, user1); var flagsRetrieved = inMemoryCache.RetrieveFlags(user1); - Assert.Equal(flags.Count, flagsRetrieved.Count); - var secondFlag = flags.Values.ToList()[1]; - var secondFlagRetrieved = flagsRetrieved.Values.ToList()[1]; - Assert.Equal(secondFlag, secondFlagRetrieved); + Assert.Equal(2, flagsRetrieved.Count); + Assert.Equal(flags["flag1"], flagsRetrieved["flag1"]); + Assert.Equal(flags["flag2"], flagsRetrieved["flag2"]); } } } From 5ef9ea2abc3463e37e30aa401ce2618d6f143ab2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 17 Jul 2018 18:17:14 -0700 Subject: [PATCH 13/47] fix default value logic --- src/LaunchDarkly.Xamarin/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index c404b742..6d017d34 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -334,7 +334,7 @@ JToken Variation(string featureKey, JToken defaultValue) { featureFlagEvent = new FeatureFlagEvent(featureKey, flag); var value = flag.value; - if (value == null) { + if (value == null || value.Type == JTokenType.Null) { featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, User, defaultValue); From c43c4bb324478bf44f2f01964746410ceecaff93 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 18 Jul 2018 15:43:54 -0700 Subject: [PATCH 14/47] add tests for event generation --- src/LaunchDarkly.Xamarin/Configuration.cs | 14 ++ src/LaunchDarkly.Xamarin/Factory.cs | 6 +- .../LdClientEventTests.cs | 137 ++++++++++++++++++ .../MockEventProcessor.cs | 19 +++ tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 2 +- 5 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs create mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index c278b592..8a584731 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net.Http; using Common.Logging; +using LaunchDarkly.Client; namespace LaunchDarkly.Xamarin { @@ -125,6 +126,7 @@ public class Configuration : IMobileConfiguration internal IFlagCacheManager FlagCacheManager { get; set; } internal IConnectionManager ConnectionManager { get; set; } + internal IEventProcessor EventProcessor { get; set; } internal IMobileUpdateProcessor MobileUpdateProcessor { get; set; } internal ISimplePersistance Persister { get; set; } internal IDeviceInfo DeviceInfo { get; set; } @@ -567,6 +569,18 @@ public static Configuration WithConnectionManager(this Configuration configurati return configuration; } + /// + /// Sets the IEventProcessor instance, used internally for stubbing mock instances. + /// + /// Configuration. + /// Event processor. + /// the same Configuration instance + public static Configuration WithEventProcessor(this Configuration configuration, IEventProcessor eventProcessor) + { + configuration.EventProcessor = eventProcessor; + return configuration; + } + /// /// Determines whether to use the Report method for networking requests /// diff --git a/src/LaunchDarkly.Xamarin/Factory.cs b/src/LaunchDarkly.Xamarin/Factory.cs index 56eb4369..5fb139c0 100644 --- a/src/LaunchDarkly.Xamarin/Factory.cs +++ b/src/LaunchDarkly.Xamarin/Factory.cs @@ -72,8 +72,12 @@ internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration confi return updateProcessor; } - internal static IEventProcessor CreateEventProcessor(IBaseConfiguration configuration) + internal static IEventProcessor CreateEventProcessor(Configuration configuration) { + if (configuration.EventProcessor != null) + { + return configuration.EventProcessor; + } if (configuration.Offline) { Log.InfoFormat("Was configured to be offline, starting service with NullEventProcessor"); diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs new file mode 100644 index 00000000..5e8c58ce --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs @@ -0,0 +1,137 @@ +using LaunchDarkly.Client; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class LdClientEventTests + { + private static readonly User user = User.WithKey("userkey"); + private MockEventProcessor eventProcessor = new MockEventProcessor(); + + public LdClient MakeClient(User user, string flagsJson) + { + Configuration config = TestUtil.ConfigWithFlagsJson(user, "appkey", flagsJson); + config.WithEventProcessor(eventProcessor); + return TestUtil.CreateClient(config, user); + } + + [Fact] + public void IdentifySendsIdentifyEvent() + { + using (LdClient client = MakeClient(user, "{}")) + { + User user1 = User.WithKey("userkey1"); + client.Identify(user1); + Assert.Collection(eventProcessor.Events, e => + { + IdentifyEvent ie = Assert.IsType(e); + Assert.Equal(user1.Key, ie.User.Key); + }); + } + } + + [Fact] + public void TrackSendsCustomEvent() + { + using (LdClient client = MakeClient(user, "{}")) + { + JToken data = new JValue("hi"); + client.Track("eventkey", data); + Assert.Collection(eventProcessor.Events, e => + { + CustomEvent ce = Assert.IsType(e); + Assert.Equal("eventkey", ce.Key); + Assert.Equal(user.Key, ce.User.Key); + Assert.Equal(data, ce.JsonData); + }); + } + } + + [Fact] + public void VariationSendsFeatureEventForValidFlag() + { + string flagsJson = @"{""flag"":{ + ""value"":""a"",""variation"":1,""version"":1000, + ""trackEvents"":true, ""debugEventsUntilDate"":2000 }}"; + using (LdClient client = MakeClient(user, flagsJson)) + { + string result = client.StringVariation("flag", "b"); + Assert.Equal("a", result); + Assert.Collection(eventProcessor.Events, e => + { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("a", fe.Value); + Assert.Equal(1, fe.Variation); + Assert.Equal(1000, fe.Version); + Assert.Equal("b", fe.Default); + Assert.True(fe.TrackEvents); + Assert.Equal(2000, fe.DebugEventsUntilDate); + }); + } + } + + [Fact] + public void FeatureEventUsesFlagVersionIfProvided() + { + string flagsJson = @"{""flag"":{ + ""value"":""a"",""variation"":1,""version"":1000, + ""flagVersion"":1500 }}"; + using (LdClient client = MakeClient(user, flagsJson)) + { + string result = client.StringVariation("flag", "b"); + Assert.Equal("a", result); + Assert.Collection(eventProcessor.Events, e => + { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("a", fe.Value); + Assert.Equal(1, fe.Variation); + Assert.Equal(1500, fe.Version); + Assert.Equal("b", fe.Default); + }); + } + } + + [Fact] + public void VariationSendsFeatureEventForDefaultValue() + { + string flagsJson = @"{""flag"":{ + ""value"":null,""variation"":null,""version"":1000 }}"; + using (LdClient client = MakeClient(user, flagsJson)) + { + string result = client.StringVariation("flag", "b"); + Assert.Equal("b", result); + Assert.Collection(eventProcessor.Events, e => + { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("b", fe.Value); + Assert.Null(fe.Variation); + Assert.Equal(1000, fe.Version); + Assert.Equal("b", fe.Default); + }); + } + } + + [Fact] + public void VariationSendsFeatureEventForUnknownFlag() + { + using (LdClient client = MakeClient(user, "{}")) + { + string result = client.StringVariation("flag", "b"); + Assert.Equal("b", result); + Assert.Collection(eventProcessor.Events, e => + { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("b", fe.Value); + Assert.Null(fe.Variation); + Assert.Null(fe.Version); + Assert.Equal("b", fe.Default); + }); + } + } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs b/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs new file mode 100644 index 00000000..1f0d3bc5 --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using LaunchDarkly.Client; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class MockEventProcessor : IEventProcessor + { + public List Events = new List(); + + public void SendEvent(Event e) + { + Events.Add(e); + } + + public void Flush() { } + + public void Dispose() { } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 25a3da41..224c7bb2 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -5,7 +5,7 @@ namespace LaunchDarkly.Xamarin.Tests { - class TestUtil + public class TestUtil { // Any tests that are going to access the static LdClient.Instance must hold this lock, // to avoid interfering with tests that use CreateClient. From 537886f0413a0acff47438cbc3c5789b8a778fc4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 19 Jul 2018 11:44:58 -0700 Subject: [PATCH 15/47] throw exception if user is null; assign unique key if key is null or empty --- src/LaunchDarkly.Xamarin/LdClient.cs | 92 ++++++++----------- .../LdClientTests.cs | 48 +++++++++- 2 files changed, 84 insertions(+), 56 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 6d017d34..f55c443d 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -58,7 +58,16 @@ public sealed class LdClient : ILdMobileClient LdClient() { } LdClient(Configuration configuration, User user) - { + { + if (configuration == null) + { + throw new ArgumentNullException("configuration"); + } + if (user == null) + { + throw new ArgumentNullException("user"); + } + Config = configuration; connectionLock = new SemaphoreSlim(1, 1); @@ -67,8 +76,8 @@ public sealed class LdClient : ILdMobileClient deviceInfo = Factory.CreateDeviceInfo(configuration); flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); - // If you pass in a null user or user with a null key, one will be assigned to them. - if (user == null || user.Key == null) + // If you pass in a user with a null or blank key, one will be assigned to them. + if (String.IsNullOrEmpty(user.Key)) { User = UserWithUniqueKey(user); } @@ -91,7 +100,7 @@ public sealed class LdClient : ILdMobileClient /// /// This constructor will wait and block on the current thread until initialization and the /// first response from the LaunchDarkly service is returned, if you would rather this happen - /// in an async fashion you can use + /// in an async fashion you can use . /// /// This is the creation point for LdClient, you must use this static method or the more specific /// to instantiate the single instance of LdClient @@ -99,7 +108,8 @@ public sealed class LdClient : ILdMobileClient /// /// The singleton LdClient instance. /// The mobile key given to you by LaunchDarkly. - /// The user needed for client operations. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. public static LdClient Init(string mobileKey, User user) { var config = Configuration.Default(mobileKey); @@ -110,7 +120,7 @@ public static LdClient Init(string mobileKey, User user) /// /// Creates and returns new LdClient singleton instance, then starts the workflow for /// fetching feature flags. This constructor should be used if you do not want to wait - /// for the IUpdateProcessor instance to finish initializing and receive the first response + /// for the client to finish initializing and receive the first response /// from the LaunchDarkly service. /// /// This is the creation point for LdClient, you must use this static method or the more specific @@ -119,7 +129,8 @@ public static LdClient Init(string mobileKey, User user) /// /// The singleton LdClient instance. /// The mobile key given to you by LaunchDarkly. - /// The user needed for client operations. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. public static async Task InitAsync(string mobileKey, User user) { var config = Configuration.Default(mobileKey); @@ -133,7 +144,7 @@ public static async Task InitAsync(string mobileKey, User user) /// /// This constructor will wait and block on the current thread until initialization and the /// first response from the LaunchDarkly service is returned, if you would rather this happen - /// in an async fashion you can use + /// in an async fashion you can use . /// /// This is the creation point for LdClient, you must use this static method or the more basic /// to instantiate the single instance of LdClient @@ -141,7 +152,8 @@ public static async Task InitAsync(string mobileKey, User user) /// /// The singleton LdClient instance. /// The client configuration object - /// The user needed for client operations. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. public static LdClient Init(Configuration config, User user) { CreateInstance(config, user); @@ -166,7 +178,8 @@ public static LdClient Init(Configuration config, User user) /// /// The singleton LdClient instance. /// The client configuration object - /// The user needed for client operations. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. public static Task InitAsync(Configuration config, User user) { CreateInstance(config, user); @@ -184,8 +197,10 @@ public static Task InitAsync(Configuration config, User user) static void CreateInstance(Configuration configuration, User user) { - if (Instance != null) - throw new Exception("LdClient instance already exists."); + if (Instance != null) + { + throw new Exception("LdClient instance already exists."); + } Instance = new LdClient(configuration, user); Log.InfoFormat("Initialized LaunchDarkly Client {0}", @@ -318,17 +333,7 @@ JToken Variation(string featureKey, JToken defaultValue) Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); return defaultValue; } - - if (User == null || User.Key == null) - { - Log.Warn("Feature flag evaluation called with null user or null user key. Returning default"); - featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, - User, - defaultValue); - eventProcessor.SendEvent(featureRequestEvent); - return defaultValue; - } - + var flag = flagCacheManager.FlagForUser(featureKey, User); if (flag != null) { @@ -372,11 +377,6 @@ public IDictionary AllFlags() Log.Warn("AllFlags() was called before client has finished initializing. Returning null."); return null; } - if (User == null || User.Key == null) - { - Log.Warn("AllFlags() called with null user or null user key. Returning null"); - return null; - } return flagCacheManager.FlagsForUser(User) .ToDictionary(p => p.Key, p => p.Value.value); @@ -385,11 +385,6 @@ public IDictionary AllFlags() /// public void Track(string eventName, JToken data) { - if (User == null || User.Key == null) - { - Log.Warn("Track called with null user or null user key"); - } - eventProcessor.SendEvent(eventFactory.NewCustomEvent(eventName, User, data)); } @@ -433,19 +428,14 @@ public async Task IdentifyAsync(User user) { if (user == null) { - Log.Warn("Identify called with null user"); - return; + throw new ArgumentNullException("user"); } - User userWithKey = null; - if (user.Key == null) + User userWithKey = user; + if (String.IsNullOrEmpty(user.Key)) { userWithKey = UserWithUniqueKey(user); } - else - { - userWithKey = user; - } await connectionLock.WaitAsync(); try @@ -488,22 +478,14 @@ void ClearUpdateProcessor() } } - User UserWithUniqueKey(User user = null) + User UserWithUniqueKey(User user) { string uniqueId = deviceInfo.UniqueDeviceId(); - - if (user != null) - { - var updatedUser = new User(user) - { - Key = uniqueId, - Anonymous = true - }; - - return updatedUser; - } - - return new User(uniqueId); + return new User(user) + { + Key = uniqueId, + Anonymous = true + }; } void IDisposable.Dispose() diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 09990da8..69c7dacf 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -21,6 +21,19 @@ public void CanCreateClientWithConfigAndUser() Assert.NotNull(Client()); } + [Fact] + public void CannotCreateClientWithNullConfig() + { + Assert.Throws(() => LdClient.Init((Configuration)null, User.WithKey("user"))); + } + + [Fact] + public void CannotCreateClientWithNullUser() + { + Configuration config = TestUtil.ConfigWithFlagsJson(User.WithKey("dummy"), appKey, "{}"); + Assert.Throws(() => LdClient.Init(config, null)); + } + [Fact] public void IdentifyUpdatesTheUser() { @@ -30,6 +43,21 @@ public void IdentifyUpdatesTheUser() Assert.Equal(client.User, updatedUser); } + [Fact] + public void IdentifyWithNullUserThrowsException() + { + var client = Client(); + Assert.Throws(() => client.Identify(null)); + } + + [Fact] + public void IdentifyAsyncWithNullUserThrowsException() + { + var client = Client(); + Assert.ThrowsAsync(async () => await client.IdentifyAsync(null)); + // note that exceptions thrown out of an async task are always wrapped in AggregateException + } + [Fact] public void SharedClientIsTheOnlyClientAvailable() { @@ -82,7 +110,16 @@ public void UserWithNullKeyWillHaveUniqueKeySet() } [Fact] - public void IdentifyWithUserMissingKeyUsesUniqueGeneratedKey() + public void UserWithEmptyKeyWillHaveUniqueKeySet() + { + var userWithEmptyKey = User.WithKey(""); + var config = TestUtil.ConfigWithFlagsJson(userWithEmptyKey, "someOtherAppKey", "{}"); + var client = TestUtil.CreateClient(config, userWithEmptyKey); + Assert.Equal(MockDeviceInfo.key, client.User.Key); + } + + [Fact] + public void IdentifyWithUserWithNullKeyUsesUniqueGeneratedKey() { var client = Client(); client.Identify(User.WithKey("a new user's key")); @@ -91,6 +128,15 @@ public void IdentifyWithUserMissingKeyUsesUniqueGeneratedKey() Assert.Equal(MockDeviceInfo.key, client.User.Key); } + [Fact] + public void IdentifyWithUserWithEmptyKeyUsesUniqueGeneratedKey() + { + var client = Client(); + var userWithEmptyKey = User.WithKey(""); + client.Identify(userWithEmptyKey); + Assert.Equal(MockDeviceInfo.key, client.User.Key); + } + [Fact] public void UpdatingKeylessUserWillGenerateNewUserWithSameValues() { From 8d299c7172bae06f36dd7a61fe39bf8b36ab1c4c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 11:33:40 -0700 Subject: [PATCH 16/47] don't use strong naming for LaunchDarkly.Xamarin --- .circleci/config.yml | 4 ---- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 8 ++------ src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs | 7 +------ .../LaunchDarkly.Xamarin.Tests.csproj | 10 +++------- 4 files changed, 6 insertions(+), 23 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a102ad9f..93ae6397 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,11 +9,7 @@ jobs: docker: - image: microsoft/dotnet:2.0-sdk-jessie steps: - - run: - name: install packages - command: apt-get -q update && apt-get install -qy awscli - checkout - - run: aws s3 cp s3://launchdarkly-pastebin/ci/dotnet/LaunchDarkly.Xamarin.snk LaunchDarkly.Xamarin.snk - run: dotnet restore - run: dotnet build src/LaunchDarkly.Xamarin -f netstandard2.0 - run: dotnet test tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj -f netcoreapp2.0 diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 702070f3..2b6ea266 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,12 +1,9 @@ - 1.0.0-beta9 + 1.0.0-beta10 netstandard1.6;netstandard2.0;net45 - true - ..\..\LaunchDarkly.Xamarin.snk - false @@ -15,14 +12,13 @@ obj\Release\netstandard2.0 - false - + diff --git a/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs b/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs index 184d6064..7d76c4d2 100644 --- a/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs +++ b/src/LaunchDarkly.Xamarin/Properties/AssemblyInfo.cs @@ -1,10 +1,5 @@ using System; using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("LaunchDarkly.Xamarin.Tests,PublicKey=" + -"0024000004800000940000000602000000240000525341310004000001000100" + -"058a1dbccbc342759dc98b1eaba4467bfdea062629f212cf7c669ff26b4e2ff3" + -"c408292487bc349b8a687d73033ff14dbf861e1eea23303a5b5d13b1db034799" + -"13bd120ba372cf961d27db9f652631565f4e8aff4a79e11cfe713833157ecb5d" + -"cbc02d772967d919f8f06fbee227a664dc591932d5b05f4da1c8439702ecfdb1")] +[assembly: InternalsVisibleTo("LaunchDarkly.Xamarin.Tests")] diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index 903ade3f..aa036d1b 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -3,21 +3,17 @@ netcoreapp2.0 2.0.0 - true - ..\..\LaunchDarkly.Xamarin.snk - true - - + - - + + From df9e61e55fe2a36778cc4b861df61a42dd5d9e10 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 12:09:48 -0700 Subject: [PATCH 17/47] skip connectivity check in .NET Standard --- .../MobileConnectionManager.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs b/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs index 9dcfa92d..3d9cccfa 100644 --- a/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs +++ b/src/LaunchDarkly.Xamarin/MobileConnectionManager.cs @@ -9,8 +9,13 @@ internal class MobileConnectionManager : IConnectionManager internal MobileConnectionManager() { - isConnected = Connectivity.NetworkAccess == NetworkAccess.Internet; - Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged; + UpdateConnectedStatus(); + try + { + Connectivity.ConnectivityChanged += Connectivity_ConnectivityChanged; + } + catch (NotImplementedInReferenceAssemblyException) + { } } bool isConnected; @@ -22,12 +27,24 @@ bool IConnectionManager.IsConnected isConnected = value; } } - - + void Connectivity_ConnectivityChanged(ConnectivityChangedEventArgs e) { - isConnected = Connectivity.NetworkAccess == NetworkAccess.Internet; + UpdateConnectedStatus(); ConnectionChanged?.Invoke(isConnected); } + + private void UpdateConnectedStatus() + { + try + { + isConnected = Connectivity.NetworkAccess == NetworkAccess.Internet; + } + catch (NotImplementedInReferenceAssemblyException) + { + // .NET Standard has no way to detect network connectivity + isConnected = true; + } + } } } From 3e4f4e12bb9ead3012d35f087159eb8b29ed7fe4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 12:11:19 -0700 Subject: [PATCH 18/47] fix name of config setter --- src/LaunchDarkly.Xamarin/Configuration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index 8a584731..3ae604df 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -239,7 +239,7 @@ public static class ConfigurationExtensions /// the configuration /// the base URI as a string /// the same Configuration instance - public static Configuration WithUri(this Configuration configuration, string uri) + public static Configuration WithBaseUri(this Configuration configuration, string uri) { if (uri != null) configuration.BaseUri = new Uri(uri); @@ -253,7 +253,7 @@ public static Configuration WithUri(this Configuration configuration, string uri /// the configuration /// the base URI /// the same Configuration instance - public static Configuration WithUri(this Configuration configuration, Uri uri) + public static Configuration WithBaseUri(this Configuration configuration, Uri uri) { if (uri != null) configuration.BaseUri = uri; From fb5621a9f840b56c5053553336d4b3c2b6765a5a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 12:53:30 -0700 Subject: [PATCH 19/47] use platform reference instead of package reference --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 2b6ea266..a38613e1 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -14,8 +14,8 @@ + - From 52bc60ae4083bbf2b2fae44923d6274fd55301d9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 12:58:36 -0700 Subject: [PATCH 20/47] skip using preferences API in .NET Standard --- .../SimpleMobileDevicePersistance.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs b/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs index 022905f6..5a0f86e1 100644 --- a/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs +++ b/src/LaunchDarkly.Xamarin/SimpleMobileDevicePersistance.cs @@ -7,12 +7,23 @@ internal class SimpleMobileDevicePersistance : ISimplePersistance { public void Save(string key, string value) { - Preferences.Set(key, value); + try + { + Preferences.Set(key, value); + } + catch (NotImplementedInReferenceAssemblyException) { } } public string GetValue(string key) { - return Preferences.Get(key, null); + try + { + return Preferences.Get(key, null); + } + catch (NotImplementedInReferenceAssemblyException) + { + return null; + } } } } From a9d5167179153958c1722a3624acf551b925d3a0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 13:20:11 -0700 Subject: [PATCH 21/47] beta11 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index a38613e1..6b163c85 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,7 +1,7 @@ - 1.0.0-beta10 + 1.0.0-beta11 netstandard1.6;netstandard2.0;net45 From fca4ca08c2774b9d5dac551d09b8d73fe4ee0af9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 13:24:44 -0700 Subject: [PATCH 22/47] fix test --- tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs index 03e2ac60..250deb68 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs @@ -11,7 +11,7 @@ public class ConfigurationTest public void CanOverrideConfiguration() { var config = Configuration.Default("AnyOtherSdkKey") - .WithUri("https://app.AnyOtherEndpoint.com") + .WithBaseUri("https://app.AnyOtherEndpoint.com") .WithEventQueueCapacity(99) .WithPollingInterval(TimeSpan.FromMinutes(1)); From a8ffc36f669fd1b540f58d0c1efd1ef0d4250aef Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 14:13:08 -0700 Subject: [PATCH 23/47] rm note about signing --- README.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/README.md b/README.md index b6617ad5..d2065665 100644 --- a/README.md +++ b/README.md @@ -49,26 +49,6 @@ Contributing See [Contributing](https://github.com/launchdarkly/xamarin-client/blob/master/CONTRIBUTING.md). -Signing -------- -The artifacts generated from this repo are signed by LaunchDarkly. The public key file is in this repo at `LaunchDarkly.Xamarin.pk` as well as here: - -``` -Public Key: -00240000048000009400000006020000 -00240000525341310004000001000100 -058a1dbccbc342759dc98b1eaba4467b -fdea062629f212cf7c669ff26b4e2ff3 -c408292487bc349b8a687d73033ff14d -bf861e1eea23303a5b5d13b1db034799 -13bd120ba372cf961d27db9f65263156 -5f4e8aff4a79e11cfe713833157ecb5d -cbc02d772967d919f8f06fbee227a664 -dc591932d5b05f4da1c8439702ecfdb1 - -Public Key Token: 90b24964a3dfb906f86add69004e6885 -``` - About LaunchDarkly ----------- From d32bdf609b43b6ee41fb9861a18c54bfbf189d89 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 14:56:18 -0700 Subject: [PATCH 24/47] don't set updateProcessor to null --- src/LaunchDarkly.Xamarin/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index f55c443d..8b0f4fa1 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -474,7 +474,7 @@ void ClearUpdateProcessor() if (updateProcessor != null) { updateProcessor.Dispose(); - updateProcessor = null; + updateProcessor = new NullUpdateProcessor(); } } From 23c94c1a1d3285a0b6dda896e23c62ddff4575a9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 14:58:10 -0700 Subject: [PATCH 25/47] more cleanup/simplification of test code --- .../LdClientTests.cs | 234 ++++++++++-------- .../MockComponents.cs | 178 +++++++++++++ .../MockConnectionManager.cs | 34 --- .../MockEventProcessor.cs | 19 -- .../MockFeatureFlagRequestor.cs | 25 -- .../MockFlagCacheManager.cs | 53 ---- .../MockPollingProcessor.cs | 30 --- .../StubbedConfigAndUserBuilder.cs | 58 ----- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 20 +- 9 files changed, 319 insertions(+), 332 deletions(-) create mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockComponents.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockConnectionManager.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockFlagCacheManager.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/MockPollingProcessor.cs delete mode 100644 tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 69c7dacf..ef74713a 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -7,55 +7,55 @@ namespace LaunchDarkly.Xamarin.Tests public class DefaultLdClientTests { static readonly string appKey = "some app key"; + static readonly User simpleUser = User.WithKey("user-key"); LdClient Client() { - User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var configuration = TestUtil.ConfigWithFlagsJson(user, appKey, "{}"); - return TestUtil.CreateClient(configuration, user); + var configuration = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + return TestUtil.CreateClient(configuration, simpleUser); } - - [Fact] - public void CanCreateClientWithConfigAndUser() - { - Assert.NotNull(Client()); - } - + [Fact] public void CannotCreateClientWithNullConfig() { - Assert.Throws(() => LdClient.Init((Configuration)null, User.WithKey("user"))); + Assert.Throws(() => LdClient.Init((Configuration)null, simpleUser)); } [Fact] public void CannotCreateClientWithNullUser() { - Configuration config = TestUtil.ConfigWithFlagsJson(User.WithKey("dummy"), appKey, "{}"); + Configuration config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); Assert.Throws(() => LdClient.Init(config, null)); } [Fact] public void IdentifyUpdatesTheUser() { - var client = Client(); - var updatedUser = User.WithKey("some new key"); - client.Identify(updatedUser); - Assert.Equal(client.User, updatedUser); + using (var client = Client()) + { + var updatedUser = User.WithKey("some new key"); + client.Identify(updatedUser); + Assert.Equal(client.User, updatedUser); + } } [Fact] public void IdentifyWithNullUserThrowsException() { - var client = Client(); - Assert.Throws(() => client.Identify(null)); + using (var client = Client()) + { + Assert.Throws(() => client.Identify(null)); + } } [Fact] public void IdentifyAsyncWithNullUserThrowsException() { - var client = Client(); - Assert.ThrowsAsync(async () => await client.IdentifyAsync(null)); - // note that exceptions thrown out of an async task are always wrapped in AggregateException + using (var client = Client()) + { + Assert.ThrowsAsync(async () => await client.IdentifyAsync(null)); + // note that exceptions thrown out of an async task are always wrapped in AggregateException + } } [Fact] @@ -63,16 +63,17 @@ public void SharedClientIsTheOnlyClientAvailable() { lock (TestUtil.ClientInstanceLock) { - User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); - var config = TestUtil.ConfigWithFlagsJson(user, appKey, "{}"); - var client = LdClient.Init(config, user); - try + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + using (var client = LdClient.Init(config, simpleUser)) { - Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); - } - finally - { - LdClient.Instance = null; + try + { + Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, simpleUser)); + } + finally + { + LdClient.Instance = null; + } } } } @@ -80,117 +81,150 @@ public void SharedClientIsTheOnlyClientAvailable() [Fact] public void ConnectionManagerShouldKnowIfOnlineOrNot() { - var client = Client(); - var connMgr = client.Config.ConnectionManager as MockConnectionManager; - connMgr.ConnectionChanged += (bool obj) => client.Online = obj; - connMgr.Connect(true); - Assert.False(client.IsOffline()); - connMgr.Connect(false); - Assert.False(client.Online); + using (var client = Client()) + { + var connMgr = client.Config.ConnectionManager as MockConnectionManager; + connMgr.ConnectionChanged += (bool obj) => client.Online = obj; + connMgr.Connect(true); + Assert.False(client.IsOffline()); + connMgr.Connect(false); + Assert.False(client.Online); + } } [Fact] public void ConnectionChangeShouldStopUpdateProcessor() { - var client = Client(); - var connMgr = client.Config.ConnectionManager as MockConnectionManager; - connMgr.ConnectionChanged += (bool obj) => client.Online = obj; - connMgr.Connect(false); - var mockUpdateProc = client.Config.MobileUpdateProcessor as MockPollingProcessor; - Assert.False(mockUpdateProc.IsRunning); + using (var client = Client()) + { + var connMgr = client.Config.ConnectionManager as MockConnectionManager; + connMgr.ConnectionChanged += (bool obj) => client.Online = obj; + connMgr.Connect(false); + var mockUpdateProc = client.Config.MobileUpdateProcessor as MockPollingProcessor; + Assert.False(mockUpdateProc.IsRunning); + } } [Fact] public void UserWithNullKeyWillHaveUniqueKeySet() { var userWithNullKey = User.WithKey(null); - var config = TestUtil.ConfigWithFlagsJson(userWithNullKey, "someOtherAppKey", "{}"); - var client = TestUtil.CreateClient(config, userWithNullKey); - Assert.Equal(MockDeviceInfo.key, client.User.Key); + var uniqueId = "some-unique-key"; + var config = TestUtil.ConfigWithFlagsJson(userWithNullKey, appKey, "{}") + .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + using (var client = TestUtil.CreateClient(config, userWithNullKey)) + { + Assert.Equal(uniqueId, client.User.Key); + Assert.True(client.User.Anonymous); + } } [Fact] public void UserWithEmptyKeyWillHaveUniqueKeySet() { var userWithEmptyKey = User.WithKey(""); - var config = TestUtil.ConfigWithFlagsJson(userWithEmptyKey, "someOtherAppKey", "{}"); - var client = TestUtil.CreateClient(config, userWithEmptyKey); - Assert.Equal(MockDeviceInfo.key, client.User.Key); + var uniqueId = "some-unique-key"; + var config = TestUtil.ConfigWithFlagsJson(userWithEmptyKey, appKey, "{}") + .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + using (var client = TestUtil.CreateClient(config, userWithEmptyKey)) + { + Assert.Equal(uniqueId, client.User.Key); + Assert.True(client.User.Anonymous); + } } [Fact] public void IdentifyWithUserWithNullKeyUsesUniqueGeneratedKey() { - var client = Client(); - client.Identify(User.WithKey("a new user's key")); var userWithNullKey = User.WithKey(null); - client.Identify(userWithNullKey); - Assert.Equal(MockDeviceInfo.key, client.User.Key); + var uniqueId = "some-unique-key"; + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + client.Identify(userWithNullKey); + Assert.Equal(uniqueId, client.User.Key); + Assert.True(client.User.Anonymous); + } } [Fact] public void IdentifyWithUserWithEmptyKeyUsesUniqueGeneratedKey() { - var client = Client(); var userWithEmptyKey = User.WithKey(""); - client.Identify(userWithEmptyKey); - Assert.Equal(MockDeviceInfo.key, client.User.Key); - } - - [Fact] - public void UpdatingKeylessUserWillGenerateNewUserWithSameValues() - { - var updatedUser = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn(String.Empty); - var client = Client(); - var previousUser = client.User; - client.Identify(updatedUser); - Assert.NotEqual(updatedUser, previousUser); - Assert.Equal(updatedUser.Avatar, previousUser.Avatar); - Assert.Equal(updatedUser.Country, previousUser.Country); - Assert.Equal(updatedUser.Email, previousUser.Email); - Assert.Equal(updatedUser.FirstName, previousUser.FirstName); - Assert.Equal(updatedUser.LastName, previousUser.LastName); - Assert.Equal(updatedUser.Name, previousUser.Name); - Assert.Equal(updatedUser.IpAddress, previousUser.IpAddress); - Assert.Equal(updatedUser.SecondaryKey, previousUser.SecondaryKey); - Assert.Equal(updatedUser.Custom["somePrivateAttr1"], previousUser.Custom["somePrivateAttr1"]); - Assert.Equal(updatedUser.Custom["somePrivateAttr2"], previousUser.Custom["somePrivateAttr2"]); + var uniqueId = "some-unique-key"; + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + client.Identify(userWithEmptyKey); + Assert.Equal(uniqueId, client.User.Key); + Assert.True(client.User.Anonymous); + } } [Fact] - public void UpdatingKeylessUserSetsAnonymousToTrue() - { - var updatedUser = User.WithKey(null); - var client = Client(); - var previousUser = client.User; - client.Identify(updatedUser); - Assert.True(client.User.Anonymous); + public void AllOtherAttributesArePreservedWhenSubstitutingUniqueUserKey() + { + var user = User.WithKey("") + .AndSecondaryKey("secondary") + .AndIpAddress("10.0.0.1") + .AndCountry("US") + .AndFirstName("John") + .AndLastName("Doe") + .AndName("John Doe") + .AndAvatar("images.google.com/myAvatar") + .AndEmail("test@example.com") + .AndCustomAttribute("attr", "value"); + var uniqueId = "some-unique-key"; + var config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}") + .WithDeviceInfo(new MockDeviceInfo(uniqueId)); + using (var client = TestUtil.CreateClient(config, simpleUser)) + { + client.Identify(user); + User newUser = client.User; + Assert.NotEqual(user.Key, newUser.Key); + Assert.Equal(user.Avatar, newUser.Avatar); + Assert.Equal(user.Country, newUser.Country); + Assert.Equal(user.Email, newUser.Email); + Assert.Equal(user.FirstName, newUser.FirstName); + Assert.Equal(user.LastName, newUser.LastName); + Assert.Equal(user.Name, newUser.Name); + Assert.Equal(user.IpAddress, newUser.IpAddress); + Assert.Equal(user.SecondaryKey, newUser.SecondaryKey); + Assert.Equal(user.Custom["attr"], newUser.Custom["attr"]); + Assert.True(newUser.Anonymous); + } } - + [Fact] public void CanRegisterListener() { - var client = Client(); - var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; - var listener = new TestListener(); - client.RegisterFeatureFlagListener("user1-flag", listener); - listenerMgr.FlagWasUpdated("user1-flag", 7); - Assert.Equal(7, listener.FeatureFlags["user1-flag"].ToObject()); + using (var client = Client()) + { + var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; + var listener = new TestListener(); + client.RegisterFeatureFlagListener("user1-flag", listener); + listenerMgr.FlagWasUpdated("user1-flag", 7); + Assert.Equal(7, listener.FeatureFlags["user1-flag"].ToObject()); + } } [Fact] public void UnregisterListenerUnregistersPassedInListenerForFlagKeyOnListenerManager() { - var client = Client(); - var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; - var listener = new TestListener(); - client.RegisterFeatureFlagListener("user2-flag", listener); - listenerMgr.FlagWasUpdated("user2-flag", 7); - Assert.Equal(7, listener.FeatureFlags["user2-flag"]); - - client.UnregisterFeatureFlagListener("user2-flag", listener); - listenerMgr.FlagWasUpdated("user2-flag", 12); - Assert.NotEqual(12, listener.FeatureFlags["user2-flag"]); + using (var client = Client()) + { + var listenerMgr = client.Config.FeatureFlagListenerManager as FeatureFlagListenerManager; + var listener = new TestListener(); + client.RegisterFeatureFlagListener("user2-flag", listener); + listenerMgr.FlagWasUpdated("user2-flag", 7); + Assert.Equal(7, listener.FeatureFlags["user2-flag"]); + + client.UnregisterFeatureFlagListener("user2-flag", listener); + listenerMgr.FlagWasUpdated("user2-flag", 12); + Assert.NotEqual(12, listener.FeatureFlags["user2-flag"]); + } } } } diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockComponents.cs b/tests/LaunchDarkly.Xamarin.Tests/MockComponents.cs new file mode 100644 index 00000000..83645b1f --- /dev/null +++ b/tests/LaunchDarkly.Xamarin.Tests/MockComponents.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LaunchDarkly.Client; + +namespace LaunchDarkly.Xamarin.Tests +{ + internal class MockConnectionManager : IConnectionManager + { + public Action ConnectionChanged; + + public MockConnectionManager(bool isOnline) + { + isConnected = isOnline; + } + + bool isConnected; + public bool IsConnected + { + get + { + return isConnected; + } + + set + { + isConnected = value; + } + } + + public void Connect(bool online) + { + IsConnected = online; + ConnectionChanged?.Invoke(IsConnected); + } + } + + internal class MockDeviceInfo : IDeviceInfo + { + private readonly string key; + + public MockDeviceInfo(string key) + { + this.key = key; + } + + public string UniqueDeviceId() + { + return key; + } + } + + internal class MockEventProcessor : IEventProcessor + { + public List Events = new List(); + + public void SendEvent(Event e) + { + Events.Add(e); + } + + public void Flush() { } + + public void Dispose() { } + } + + internal class MockFeatureFlagRequestor : IFeatureFlagRequestor + { + private readonly string _jsonFlags; + + public MockFeatureFlagRequestor(string jsonFlags) + { + _jsonFlags = jsonFlags; + } + + public void Dispose() + { + + } + + public Task FeatureFlagsAsync() + { + var response = new WebResponse(200, _jsonFlags, null); + return Task.FromResult(response); + } + } + + internal class MockFlagCacheManager : IFlagCacheManager + { + private readonly IUserFlagCache _flagCache; + + public MockFlagCacheManager(IUserFlagCache flagCache) + { + _flagCache = flagCache; + } + + public void CacheFlagsFromService(IDictionary flags, User user) + { + _flagCache.CacheFlagsForUser(flags, user); + } + + public FeatureFlag FlagForUser(string flagKey, User user) + { + var flags = FlagsForUser(user); + FeatureFlag featureFlag; + if (flags.TryGetValue(flagKey, out featureFlag)) + { + return featureFlag; + } + + return null; + } + + public IDictionary FlagsForUser(User user) + { + return _flagCache.RetrieveFlags(user); + } + + public void RemoveFlagForUser(string flagKey, User user) + { + var flagsForUser = FlagsForUser(user); + flagsForUser.Remove(flagKey); + + CacheFlagsFromService(flagsForUser, user); + } + + public void UpdateFlagForUser(string flagKey, FeatureFlag featureFlag, User user) + { + var flagsForUser = FlagsForUser(user); + flagsForUser[flagKey] = featureFlag; + + CacheFlagsFromService(flagsForUser, user); + } + } + + internal class MockPersister : ISimplePersistance + { + private IDictionary map = new Dictionary(); + + public string GetValue(string key) + { + if (!map.ContainsKey(key)) + return null; + + return map[key]; + } + + public void Save(string key, string value) + { + map[key] = value; + } + } + + internal class MockPollingProcessor : IMobileUpdateProcessor + { + public bool IsRunning + { + get; + set; + } + + public void Dispose() + { + IsRunning = false; + } + + public bool Initialized() + { + return IsRunning; + } + + public Task Start() + { + IsRunning = true; + return Task.FromResult(true); + } + } +} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockConnectionManager.cs b/tests/LaunchDarkly.Xamarin.Tests/MockConnectionManager.cs deleted file mode 100644 index cb53ed37..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/MockConnectionManager.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace LaunchDarkly.Xamarin.Tests -{ - internal class MockConnectionManager : IConnectionManager - { - public Action ConnectionChanged; - - public MockConnectionManager(bool isOnline) - { - isConnected = isOnline; - } - - bool isConnected; - public bool IsConnected - { - get - { - return isConnected; - } - - set - { - isConnected = value; - } - } - - public void Connect(bool online) - { - IsConnected = online; - ConnectionChanged?.Invoke(IsConnected); - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs b/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs deleted file mode 100644 index 1f0d3bc5..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/MockEventProcessor.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using LaunchDarkly.Client; - -namespace LaunchDarkly.Xamarin.Tests -{ - public class MockEventProcessor : IEventProcessor - { - public List Events = new List(); - - public void SendEvent(Event e) - { - Events.Add(e); - } - - public void Flush() { } - - public void Dispose() { } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs b/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs deleted file mode 100644 index 59b6c964..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/MockFeatureFlagRequestor.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading.Tasks; - -namespace LaunchDarkly.Xamarin.Tests -{ - internal class MockFeatureFlagRequestor : IFeatureFlagRequestor - { - private readonly string _jsonFlags; - - public MockFeatureFlagRequestor(string jsonFlags) - { - _jsonFlags = jsonFlags; - } - - public void Dispose() - { - - } - - public Task FeatureFlagsAsync() - { - var response = new WebResponse(200, _jsonFlags, null); - return Task.FromResult(response); - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockFlagCacheManager.cs b/tests/LaunchDarkly.Xamarin.Tests/MockFlagCacheManager.cs deleted file mode 100644 index 936c1f46..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/MockFlagCacheManager.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using LaunchDarkly.Client; - -namespace LaunchDarkly.Xamarin.Tests -{ - internal class MockFlagCacheManager : IFlagCacheManager - { - private readonly IUserFlagCache _flagCache; - - public MockFlagCacheManager(IUserFlagCache flagCache) - { - _flagCache = flagCache; - } - - public void CacheFlagsFromService(IDictionary flags, User user) - { - _flagCache.CacheFlagsForUser(flags, user); - } - - public FeatureFlag FlagForUser(string flagKey, User user) - { - var flags = FlagsForUser(user); - FeatureFlag featureFlag; - if (flags.TryGetValue(flagKey, out featureFlag)) - { - return featureFlag; - } - - return null; - } - - public IDictionary FlagsForUser(User user) - { - return _flagCache.RetrieveFlags(user); - } - - public void RemoveFlagForUser(string flagKey, User user) - { - var flagsForUser = FlagsForUser(user); - flagsForUser.Remove(flagKey); - - CacheFlagsFromService(flagsForUser, user); - } - - public void UpdateFlagForUser(string flagKey, FeatureFlag featureFlag, User user) - { - var flagsForUser = FlagsForUser(user); - flagsForUser[flagKey] = featureFlag; - - CacheFlagsFromService(flagsForUser, user); - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/MockPollingProcessor.cs b/tests/LaunchDarkly.Xamarin.Tests/MockPollingProcessor.cs deleted file mode 100644 index 5bbd9105..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/MockPollingProcessor.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace LaunchDarkly.Xamarin.Tests -{ - public class MockPollingProcessor : IMobileUpdateProcessor - { - public bool IsRunning - { - get; - set; - } - - public void Dispose() - { - IsRunning = false; - } - - public bool Initialized() - { - return IsRunning; - } - - public Task Start() - { - IsRunning = true; - return Task.FromResult(true); - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs b/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs deleted file mode 100644 index 5622e9d3..00000000 --- a/tests/LaunchDarkly.Xamarin.Tests/StubbedConfigAndUserBuilder.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using LaunchDarkly.Client; -using Newtonsoft.Json.Linq; - -namespace LaunchDarkly.Xamarin.Tests -{ - public static class StubbedConfigAndUserBuilder - { - public static User UserWithAllPropertiesFilledIn(string key) - { - var user = User.WithKey(key); - user.SecondaryKey = "secondaryKey"; - user.IpAddress = "10.0.0.1"; - user.Country = "US"; - user.FirstName = "John"; - user.LastName = "Doe"; - user.Name = user.FirstName + " " + user.LastName; - user.Avatar = "images.google.com/myAvatar"; - user.Email = "someEmail@google.com"; - user.Custom = new Dictionary - { - {"somePrivateAttr1", JToken.FromObject("attributeValue1")}, - {"somePrivateAttr2", JToken.FromObject("attributeValue2")}, - }; - - return user; - } - } - - public class MockPersister : ISimplePersistance - { - private IDictionary map = new Dictionary(); - - public string GetValue(string key) - { - if (!map.ContainsKey(key)) - return null; - - return map[key]; - } - - public void Save(string key, string value) - { - map[key] = value; - } - } - - public class MockDeviceInfo : IDeviceInfo - { - public const string key = "someUniqueKey"; - - public string UniqueDeviceId() - { - return key; - } - } -} diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 224c7bb2..b4e20d86 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -44,21 +44,15 @@ public static Configuration ConfigWithFlagsJson(User user, string appKey, string { stubbedFlagCache.CacheFlagsForUser(flags, user); } - - var mockOnlineConnectionManager = new MockConnectionManager(true); - var mockFlagCacheManager = new MockFlagCacheManager(stubbedFlagCache); - var mockPollingProcessor = new MockPollingProcessor(); - var mockPersister = new MockPersister(); - var mockDeviceInfo = new MockDeviceInfo(); - var featureFlagListener = new FeatureFlagListenerManager(); Configuration configuration = Configuration.Default(appKey) - .WithFlagCacheManager(mockFlagCacheManager) - .WithConnectionManager(mockOnlineConnectionManager) - .WithUpdateProcessor(mockPollingProcessor) - .WithPersister(mockPersister) - .WithDeviceInfo(mockDeviceInfo) - .WithFeatureFlagListenerManager(featureFlagListener); + .WithFlagCacheManager(new MockFlagCacheManager(stubbedFlagCache)) + .WithConnectionManager(new MockConnectionManager(true)) + .WithEventProcessor(new MockEventProcessor()) + .WithUpdateProcessor(new MockPollingProcessor()) + .WithPersister(new MockPersister()) + .WithDeviceInfo(new MockDeviceInfo("")) + .WithFeatureFlagListenerManager(new FeatureFlagListenerManager()); return configuration; } } From c0cd164a9250dcd0049a2d205fbaa951c20a8ee8 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Jul 2018 16:42:53 -0700 Subject: [PATCH 26/47] make timeout a parameter instead of a config property --- src/LaunchDarkly.Xamarin/Configuration.cs | 25 ------- src/LaunchDarkly.Xamarin/ILdMobileClient.cs | 6 +- src/LaunchDarkly.Xamarin/LdClient.cs | 65 +++++++------------ .../LdClientTests.cs | 6 +- tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs | 5 +- 5 files changed, 32 insertions(+), 75 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index 8a584731..6c777ff5 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -60,12 +60,6 @@ public class Configuration : IMobileConfiguration /// public TimeSpan PollingInterval { get; internal set; } /// - /// How long the client constructor will block awaiting a successful connection to - /// LaunchDarkly. Setting this to 0 will not block and will cause the constructor to return - /// immediately. The default value is 5 seconds. - /// - public TimeSpan StartWaitTime { get; internal set; } - /// /// The timeout when reading data from the EventSource API. The default value is 5 minutes. /// public TimeSpan ReadTimeout { get; internal set; } @@ -157,10 +151,6 @@ public class Configuration : IMobileConfiguration /// private static readonly TimeSpan DefaultEventQueueFrequency = TimeSpan.FromSeconds(5); /// - /// Default value for . - /// - private static readonly TimeSpan DefaultStartWaitTime = TimeSpan.FromSeconds(5); - /// /// Default value for . /// private static readonly TimeSpan DefaultReadTimeout = TimeSpan.FromMinutes(5); @@ -205,7 +195,6 @@ public static Configuration Default(string mobileKey) EventQueueCapacity = DefaultEventQueueCapacity, EventQueueFrequency = DefaultEventQueueFrequency, PollingInterval = DefaultPollingInterval, - StartWaitTime = DefaultStartWaitTime, ReadTimeout = DefaultReadTimeout, ReconnectTime = DefaultReconnectTime, HttpClientTimeout = DefaultHttpClientTimeout, @@ -382,20 +371,6 @@ public static Configuration WithPollingInterval(this Configuration configuration return configuration; } - /// - /// Sets how long the client constructor will block awaiting a successful connection to - /// LaunchDarkly. Setting this to 0 will not block and will cause the constructor to return - /// immediately. The default value is 5 seconds. - /// - /// the configuration - /// the length of time to wait - /// the same Configuration instance - public static Configuration WithStartWaitTime(this Configuration configuration, TimeSpan startWaitTime) - { - configuration.StartWaitTime = startWaitTime; - return configuration; - } - /// /// Sets whether or not this client is offline. If true, no calls to Launchdarkly will be made. /// diff --git a/src/LaunchDarkly.Xamarin/ILdMobileClient.cs b/src/LaunchDarkly.Xamarin/ILdMobileClient.cs index faac2ecc..39e7b26d 100644 --- a/src/LaunchDarkly.Xamarin/ILdMobileClient.cs +++ b/src/LaunchDarkly.Xamarin/ILdMobileClient.cs @@ -75,8 +75,10 @@ public interface ILdMobileClient : ILdCommonClient /// /// Gets or sets the online status of the client. /// - /// The setter will block and wait on the current thread for the Update processor - /// to either be stopped or started. + /// The setter is equivalent to calling . If you are going from offline + /// to online, and you want to wait until the connection has been established, call + /// and then use await or call Wait() on + /// its return value. /// /// true if online; otherwise, false. bool Online { get; set; } diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index f55c443d..59de7ce4 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -99,8 +99,8 @@ public sealed class LdClient : ILdMobileClient /// fetching feature flags. /// /// This constructor will wait and block on the current thread until initialization and the - /// first response from the LaunchDarkly service is returned, if you would rather this happen - /// in an async fashion you can use . + /// first response from the LaunchDarkly service is returned, up to the specified timeout. + /// If you would rather this happen in an async fashion you can use . /// /// This is the creation point for LdClient, you must use this static method or the more specific /// to instantiate the single instance of LdClient @@ -110,11 +110,14 @@ public sealed class LdClient : ILdMobileClient /// The mobile key given to you by LaunchDarkly. /// The user needed for client operations. Must not be null. /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - public static LdClient Init(string mobileKey, User user) + /// The maximum length of time to wait for the client to initialize. + /// If this time elapses, the method will not throw an exception but will return the client in + /// an uninitialized state. + public static LdClient Init(string mobileKey, User user, TimeSpan maxWaitTime) { var config = Configuration.Default(mobileKey); - return Init(config, user); + return Init(config, user, maxWaitTime); } /// @@ -143,8 +146,8 @@ public static async Task InitAsync(string mobileKey, User user) /// fetching Feature Flags. /// /// This constructor will wait and block on the current thread until initialization and the - /// first response from the LaunchDarkly service is returned, if you would rather this happen - /// in an async fashion you can use . + /// first response from the LaunchDarkly service is returned, up to the specified timeout. + /// If you would rather this happen in an async fashion you can use . /// /// This is the creation point for LdClient, you must use this static method or the more basic /// to instantiate the single instance of LdClient @@ -154,13 +157,20 @@ public static async Task InitAsync(string mobileKey, User user) /// The client configuration object /// The user needed for client operations. Must not be null. /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - public static LdClient Init(Configuration config, User user) + /// The maximum length of time to wait for the client to initialize. + /// If this time elapses, the method will not throw an exception but will return the client in + /// an uninitialized state. + public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTime) { CreateInstance(config, user); if (Instance.Online) { - Instance.StartUpdateProcessor(); + if (!Instance.StartUpdateProcessor(maxWaitTime)) + { + Log.WarnFormat("Client did not successfully initialize within {0} milliseconds.", + maxWaitTime.TotalMilliseconds); + } } return Instance; @@ -207,10 +217,10 @@ static void CreateInstance(Configuration configuration, User user) Instance.Version); } - void StartUpdateProcessor() + bool StartUpdateProcessor(TimeSpan maxWaitTime) { var initTask = updateProcessor.Start(); - var unused = initTask.Wait(Config.StartWaitTime); + return initTask.Wait(maxWaitTime); } Task StartUpdateProcessorAsync() @@ -233,13 +243,10 @@ void SetupConnectionManager() /// public bool Online { - get - { - return online; - } + get => online; set { - SetOnlineAsync(value).Wait(); + var doNotAwaitResult = SetOnlineAsync(value); } } @@ -451,12 +458,6 @@ public async Task IdentifyAsync(User user) eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(userWithKey)); } - void RestartUpdateProcessor() - { - ClearAndSetUpdateProcessor(); - StartUpdateProcessor(); - } - async Task RestartUpdateProcessorAsync() { ClearAndSetUpdateProcessor(); @@ -525,22 +526,6 @@ public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener l flagListenerManager.UnregisterListener(listener, flagKey); } - internal void EnterBackground() - { - // if using Streaming, processor needs to be reset - if (Config.IsStreamingEnabled) - { - ClearUpdateProcessor(); - Config.IsStreamingEnabled = false; - RestartUpdateProcessor(); - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); - } - else - { - PingPollingProcessor(); - } - } - internal async Task EnterBackgroundAsync() { // if using Streaming, processor needs to be reset @@ -557,12 +542,6 @@ internal async Task EnterBackgroundAsync() } } - internal void EnterForeground() - { - ResetProcessorForForeground(); - RestartUpdateProcessor(); - } - internal async Task EnterForegroundAsync() { ResetProcessorForForeground(); diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index 69c7dacf..7c4ea07d 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -24,14 +24,14 @@ public void CanCreateClientWithConfigAndUser() [Fact] public void CannotCreateClientWithNullConfig() { - Assert.Throws(() => LdClient.Init((Configuration)null, User.WithKey("user"))); + Assert.Throws(() => LdClient.Init((Configuration)null, User.WithKey("user"), TimeSpan.Zero)); } [Fact] public void CannotCreateClientWithNullUser() { Configuration config = TestUtil.ConfigWithFlagsJson(User.WithKey("dummy"), appKey, "{}"); - Assert.Throws(() => LdClient.Init(config, null)); + Assert.Throws(() => LdClient.Init(config, null, TimeSpan.Zero)); } [Fact] @@ -65,7 +65,7 @@ public void SharedClientIsTheOnlyClientAvailable() { User user = StubbedConfigAndUserBuilder.UserWithAllPropertiesFilledIn("user1Key"); var config = TestUtil.ConfigWithFlagsJson(user, appKey, "{}"); - var client = LdClient.Init(config, user); + var client = LdClient.Init(config, user, TimeSpan.Zero); try { Assert.ThrowsAsync(async () => await LdClient.InitAsync(config, User.WithKey("otherUserKey"))); diff --git a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs index 224c7bb2..af8e9005 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/TestUtil.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using LaunchDarkly.Client; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -18,7 +19,7 @@ public static LdClient CreateClient(Configuration config, User user) { lock (ClientInstanceLock) { - LdClient client = LdClient.Init(config, user); + LdClient client = LdClient.Init(config, user, TimeSpan.Zero); LdClient.Instance = null; return client; } From 5305820b0f179aba2b7e52994b0b282d79883481 Mon Sep 17 00:00:00 2001 From: Andrew Shannon Brown Date: Mon, 23 Jul 2018 16:47:54 -0700 Subject: [PATCH 27/47] Remove @ashanbrown from codeowners --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 44429ee1..8b137891 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @ashanbrown + From 77816f2e1fb4ee54ce462957982d6037a275616f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 24 Jul 2018 11:38:51 -0700 Subject: [PATCH 28/47] propagate exception if polling task fails permanently --- src/LaunchDarkly.Xamarin/LdClient.cs | 27 +++++++++++++++++-- .../MobilePollingProcessor.cs | 10 ++++--- .../LdClientTests.cs | 2 +- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 1b76f165..2ee0ff77 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -220,7 +220,14 @@ static void CreateInstance(Configuration configuration, User user) bool StartUpdateProcessor(TimeSpan maxWaitTime) { var initTask = updateProcessor.Start(); - return initTask.Wait(maxWaitTime); + try + { + return initTask.Wait(maxWaitTime); + } + catch (AggregateException e) + { + throw UnwrapAggregateException(e); + } } Task StartUpdateProcessorAsync() @@ -427,7 +434,14 @@ public void Flush() /// public void Identify(User user) { - IdentifyAsync(user).Wait(); + try + { + IdentifyAsync(user).Wait(); + } + catch (AggregateException e) + { + throw UnwrapAggregateException(e); + } } /// @@ -587,5 +601,14 @@ async Task PingPollingProcessorAsync() await pollingProcessor.PingAndWait(); } } + + private Exception UnwrapAggregateException(AggregateException e) + { + if (e.InnerExceptions.Count == 1) + { + return e.InnerExceptions[0]; + } + return e; + } } } \ No newline at end of file diff --git a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.cs b/src/LaunchDarkly.Xamarin/MobilePollingProcessor.cs index 65f13572..b36e341c 100644 --- a/src/LaunchDarkly.Xamarin/MobilePollingProcessor.cs +++ b/src/LaunchDarkly.Xamarin/MobilePollingProcessor.cs @@ -19,8 +19,8 @@ internal class MobilePollingProcessor : IMobileUpdateProcessor private readonly TimeSpan pollingInterval; private readonly TaskCompletionSource _startTask; private readonly TaskCompletionSource _stopTask; - private static int UNINITIALIZED = 0; - private static int INITIALIZED = 1; + private const int UNINITIALIZED = 0; + private const int INITIALIZED = 1; private int _initialized = UNINITIALIZED; private volatile bool _disposed; @@ -80,7 +80,7 @@ private async Task UpdateTaskAsync() _flagCacheManager.CacheFlagsFromService(flagsDictionary, user); //We can't use bool in CompareExchange because it is not a reference type. - if (Interlocked.CompareExchange(ref _initialized, INITIALIZED, UNINITIALIZED) == 0) + if (Interlocked.CompareExchange(ref _initialized, INITIALIZED, UNINITIALIZED) == UNINITIALIZED) { _startTask.SetResult(true); Log.Info("Initialized LaunchDarkly Polling Processor."); @@ -91,6 +91,10 @@ private async Task UpdateTaskAsync() { Log.ErrorFormat("Error Updating features: '{0}'", Util.ExceptionMessage(ex)); Log.Error("Received 401 error, no further polling requests will be made since SDK key is invalid"); + if (_initialized == UNINITIALIZED) + { + _startTask.SetException(ex); + } ((IDisposable)this).Dispose(); } catch (Exception ex) diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index f164bcd9..e9cb9d63 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -44,7 +44,7 @@ public void IdentifyWithNullUserThrowsException() { using (var client = Client()) { - Assert.Throws(() => client.Identify(null)); + Assert.Throws(() => client.Identify(null)); } } From 05ae673a930ce3ecaa32421dacbe3bf8a09bd568 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 24 Jul 2018 13:39:16 -0700 Subject: [PATCH 29/47] validate maxWaitTime --- src/LaunchDarkly.Xamarin/LdClient.cs | 5 +++++ .../LdClientTests.cs | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 1b76f165..d32d055e 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -162,6 +162,11 @@ public static async Task InitAsync(string mobileKey, User user) /// an uninitialized state. public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTime) { + if (maxWaitTime.Ticks < 0 && maxWaitTime != Timeout.InfiniteTimeSpan) + { + throw new ArgumentOutOfRangeException(nameof(maxWaitTime)); + } + CreateInstance(config, user); if (Instance.Online) diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs index f164bcd9..85ba446f 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientTests.cs @@ -28,6 +28,27 @@ public void CannotCreateClientWithNullUser() Assert.Throws(() => LdClient.Init(config, null, TimeSpan.Zero)); } + [Fact] + public void CannotCreateClientWithNegativeWaitTime() + { + Configuration config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + Assert.Throws(() => LdClient.Init(config, simpleUser, TimeSpan.FromMilliseconds(-2))); + } + + [Fact] + public void CanCreateClientWithInfiniteWaitTime() + { + Configuration config = TestUtil.ConfigWithFlagsJson(simpleUser, appKey, "{}"); + try + { + using (var client = LdClient.Init(config, simpleUser, System.Threading.Timeout.InfiniteTimeSpan)) { } + } + finally + { + LdClient.Instance = null; + } + } + [Fact] public void IdentifyUpdatesTheUser() { From f2ac11e38f0ddafbe54fef641c2a291c56c27deb Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 24 Jul 2018 13:51:41 -0700 Subject: [PATCH 30/47] send an initial identify event when client is created --- src/LaunchDarkly.Xamarin/LdClient.cs | 1152 +++++++++-------- .../LdClientEventTests.cs | 109 +- 2 files changed, 636 insertions(+), 625 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index d32d055e..323c5b3b 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -1,596 +1,598 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Common.Logging; -using LaunchDarkly.Client; -using LaunchDarkly.Common; -using Newtonsoft.Json.Linq; - -namespace LaunchDarkly.Xamarin -{ - /// - /// A client for the LaunchDarkly API. Client instances are thread-safe. Your application should instantiate - /// a single LdClient for the lifetime of their application. - /// - public sealed class LdClient : ILdMobileClient - { - private static readonly ILog Log = LogManager.GetLogger(typeof(LdClient)); - - /// - /// The singleton instance used by your application throughout its lifetime, can only be created once. - /// - /// Use the designated static method - /// to set this LdClient instance. - /// - /// The LdClient instance. - public static LdClient Instance { get; internal set; } - - /// - /// The Configuration instance used to setup the LdClient. - /// - /// The Configuration instance. - public Configuration Config { get; private set; } - - /// - /// The User for the LdClient operations. - /// - /// The User. - public User User { get; private set; } - - object myLockObjForConnectionChange = new object(); - object myLockObjForUserUpdate = new object(); - - IFlagCacheManager flagCacheManager; - IConnectionManager connectionManager; - IMobileUpdateProcessor updateProcessor; - IEventProcessor eventProcessor; - ISimplePersistance persister; - IDeviceInfo deviceInfo; - EventFactory eventFactory = EventFactory.Default; - IFeatureFlagListenerManager flagListenerManager; - - SemaphoreSlim connectionLock; - - // private constructor prevents initialization of this class - // without using WithConfigAnduser(config, user) - LdClient() { } - - LdClient(Configuration configuration, User user) +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Common.Logging; +using LaunchDarkly.Client; +using LaunchDarkly.Common; +using Newtonsoft.Json.Linq; + +namespace LaunchDarkly.Xamarin +{ + /// + /// A client for the LaunchDarkly API. Client instances are thread-safe. Your application should instantiate + /// a single LdClient for the lifetime of their application. + /// + public sealed class LdClient : ILdMobileClient + { + private static readonly ILog Log = LogManager.GetLogger(typeof(LdClient)); + + /// + /// The singleton instance used by your application throughout its lifetime, can only be created once. + /// + /// Use the designated static method + /// to set this LdClient instance. + /// + /// The LdClient instance. + public static LdClient Instance { get; internal set; } + + /// + /// The Configuration instance used to setup the LdClient. + /// + /// The Configuration instance. + public Configuration Config { get; private set; } + + /// + /// The User for the LdClient operations. + /// + /// The User. + public User User { get; private set; } + + object myLockObjForConnectionChange = new object(); + object myLockObjForUserUpdate = new object(); + + IFlagCacheManager flagCacheManager; + IConnectionManager connectionManager; + IMobileUpdateProcessor updateProcessor; + IEventProcessor eventProcessor; + ISimplePersistance persister; + IDeviceInfo deviceInfo; + EventFactory eventFactory = EventFactory.Default; + IFeatureFlagListenerManager flagListenerManager; + + SemaphoreSlim connectionLock; + + // private constructor prevents initialization of this class + // without using WithConfigAnduser(config, user) + LdClient() { } + + LdClient(Configuration configuration, User user) { if (configuration == null) { throw new ArgumentNullException("configuration"); - } + } if (user == null) { throw new ArgumentNullException("user"); - } - - Config = configuration; - - connectionLock = new SemaphoreSlim(1, 1); - - persister = Factory.CreatePersister(configuration); - deviceInfo = Factory.CreateDeviceInfo(configuration); - flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); - - // If you pass in a user with a null or blank key, one will be assigned to them. - if (String.IsNullOrEmpty(user.Key)) - { - User = UserWithUniqueKey(user); - } - else - { - User = user; - } - - flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagListenerManager, User); - connectionManager = Factory.CreateConnectionManager(configuration); - updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager); - eventProcessor = Factory.CreateEventProcessor(configuration); - - SetupConnectionManager(); - } - - /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for - /// fetching feature flags. - /// - /// This constructor will wait and block on the current thread until initialization and the - /// first response from the LaunchDarkly service is returned, up to the specified timeout. - /// If you would rather this happen in an async fashion you can use . - /// - /// This is the creation point for LdClient, you must use this static method or the more specific - /// to instantiate the single instance of LdClient - /// for the lifetime of your application. - /// - /// The singleton LdClient instance. - /// The mobile key given to you by LaunchDarkly. - /// The user needed for client operations. Must not be null. - /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - /// The maximum length of time to wait for the client to initialize. - /// If this time elapses, the method will not throw an exception but will return the client in - /// an uninitialized state. - public static LdClient Init(string mobileKey, User user, TimeSpan maxWaitTime) - { - var config = Configuration.Default(mobileKey); - - return Init(config, user, maxWaitTime); - } - - /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for - /// fetching feature flags. This constructor should be used if you do not want to wait - /// for the client to finish initializing and receive the first response - /// from the LaunchDarkly service. - /// - /// This is the creation point for LdClient, you must use this static method or the more specific - /// to instantiate the single instance of LdClient - /// for the lifetime of your application. - /// - /// The singleton LdClient instance. - /// The mobile key given to you by LaunchDarkly. - /// The user needed for client operations. Must not be null. - /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - public static async Task InitAsync(string mobileKey, User user) - { - var config = Configuration.Default(mobileKey); - - return await InitAsync(config, user); - } - - /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for - /// fetching Feature Flags. - /// - /// This constructor will wait and block on the current thread until initialization and the - /// first response from the LaunchDarkly service is returned, up to the specified timeout. - /// If you would rather this happen in an async fashion you can use . - /// - /// This is the creation point for LdClient, you must use this static method or the more basic - /// to instantiate the single instance of LdClient - /// for the lifetime of your application. - /// - /// The singleton LdClient instance. - /// The client configuration object - /// The user needed for client operations. Must not be null. - /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - /// The maximum length of time to wait for the client to initialize. - /// If this time elapses, the method will not throw an exception but will return the client in - /// an uninitialized state. - public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTime) - { + } + + Config = configuration; + + connectionLock = new SemaphoreSlim(1, 1); + + persister = Factory.CreatePersister(configuration); + deviceInfo = Factory.CreateDeviceInfo(configuration); + flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); + + // If you pass in a user with a null or blank key, one will be assigned to them. + if (String.IsNullOrEmpty(user.Key)) + { + User = UserWithUniqueKey(user); + } + else + { + User = user; + } + + flagCacheManager = Factory.CreateFlagCacheManager(configuration, persister, flagListenerManager, User); + connectionManager = Factory.CreateConnectionManager(configuration); + updateProcessor = Factory.CreateUpdateProcessor(configuration, User, flagCacheManager); + eventProcessor = Factory.CreateEventProcessor(configuration); + + eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(User)); + + SetupConnectionManager(); + } + + /// + /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// fetching feature flags. + /// + /// This constructor will wait and block on the current thread until initialization and the + /// first response from the LaunchDarkly service is returned, up to the specified timeout. + /// If you would rather this happen in an async fashion you can use . + /// + /// This is the creation point for LdClient, you must use this static method or the more specific + /// to instantiate the single instance of LdClient + /// for the lifetime of your application. + /// + /// The singleton LdClient instance. + /// The mobile key given to you by LaunchDarkly. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. + /// The maximum length of time to wait for the client to initialize. + /// If this time elapses, the method will not throw an exception but will return the client in + /// an uninitialized state. + public static LdClient Init(string mobileKey, User user, TimeSpan maxWaitTime) + { + var config = Configuration.Default(mobileKey); + + return Init(config, user, maxWaitTime); + } + + /// + /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// fetching feature flags. This constructor should be used if you do not want to wait + /// for the client to finish initializing and receive the first response + /// from the LaunchDarkly service. + /// + /// This is the creation point for LdClient, you must use this static method or the more specific + /// to instantiate the single instance of LdClient + /// for the lifetime of your application. + /// + /// The singleton LdClient instance. + /// The mobile key given to you by LaunchDarkly. + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. + public static async Task InitAsync(string mobileKey, User user) + { + var config = Configuration.Default(mobileKey); + + return await InitAsync(config, user); + } + + /// + /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// fetching Feature Flags. + /// + /// This constructor will wait and block on the current thread until initialization and the + /// first response from the LaunchDarkly service is returned, up to the specified timeout. + /// If you would rather this happen in an async fashion you can use . + /// + /// This is the creation point for LdClient, you must use this static method or the more basic + /// to instantiate the single instance of LdClient + /// for the lifetime of your application. + /// + /// The singleton LdClient instance. + /// The client configuration object + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. + /// The maximum length of time to wait for the client to initialize. + /// If this time elapses, the method will not throw an exception but will return the client in + /// an uninitialized state. + public static LdClient Init(Configuration config, User user, TimeSpan maxWaitTime) + { if (maxWaitTime.Ticks < 0 && maxWaitTime != Timeout.InfiniteTimeSpan) { throw new ArgumentOutOfRangeException(nameof(maxWaitTime)); - } - - CreateInstance(config, user); - - if (Instance.Online) - { + } + + CreateInstance(config, user); + + if (Instance.Online) + { if (!Instance.StartUpdateProcessor(maxWaitTime)) { Log.WarnFormat("Client did not successfully initialize within {0} milliseconds.", maxWaitTime.TotalMilliseconds); - } - } - - return Instance; - } - - /// - /// Creates and returns new LdClient singleton instance, then starts the workflow for - /// fetching Feature Flags. This constructor should be used if you do not want to wait - /// for the IUpdateProcessor instance to finish initializing and receive the first response - /// from the LaunchDarkly service. - /// - /// This is the creation point for LdClient, you must use this static method or the more basic - /// to instantiate the single instance of LdClient - /// for the lifetime of your application. - /// - /// The singleton LdClient instance. - /// The client configuration object - /// The user needed for client operations. Must not be null. - /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. - public static Task InitAsync(Configuration config, User user) - { - CreateInstance(config, user); - - if (Instance.Online) - { - Task t = Instance.StartUpdateProcessorAsync(); - return t.ContinueWith((result) => Instance); - } - else - { - return Task.FromResult(Instance); - } - } - - static void CreateInstance(Configuration configuration, User user) - { + } + } + + return Instance; + } + + /// + /// Creates and returns new LdClient singleton instance, then starts the workflow for + /// fetching Feature Flags. This constructor should be used if you do not want to wait + /// for the IUpdateProcessor instance to finish initializing and receive the first response + /// from the LaunchDarkly service. + /// + /// This is the creation point for LdClient, you must use this static method or the more basic + /// to instantiate the single instance of LdClient + /// for the lifetime of your application. + /// + /// The singleton LdClient instance. + /// The client configuration object + /// The user needed for client operations. Must not be null. + /// If the user's Key is null, it will be assigned a key that uniquely identifies this device. + public static Task InitAsync(Configuration config, User user) + { + CreateInstance(config, user); + + if (Instance.Online) + { + Task t = Instance.StartUpdateProcessorAsync(); + return t.ContinueWith((result) => Instance); + } + else + { + return Task.FromResult(Instance); + } + } + + static void CreateInstance(Configuration configuration, User user) + { if (Instance != null) - { + { throw new Exception("LdClient instance already exists."); - } - - Instance = new LdClient(configuration, user); - Log.InfoFormat("Initialized LaunchDarkly Client {0}", - Instance.Version); - } - - bool StartUpdateProcessor(TimeSpan maxWaitTime) - { - var initTask = updateProcessor.Start(); - return initTask.Wait(maxWaitTime); - } - - Task StartUpdateProcessorAsync() - { - return updateProcessor.Start(); - } - - void SetupConnectionManager() - { - if (connectionManager is MobileConnectionManager mobileConnectionManager) - { - mobileConnectionManager.ConnectionChanged += MobileConnectionManager_ConnectionChanged; - Log.InfoFormat("The mobile client connection changed online to {0}", - connectionManager.IsConnected); - } - online = connectionManager.IsConnected; - } - - bool online; - /// - public bool Online - { - get => online; - set - { - var doNotAwaitResult = SetOnlineAsync(value); - } - } - - public async Task SetOnlineAsync(bool value) - { - await connectionLock.WaitAsync(); - online = value; - try - { - if (online) - { - await RestartUpdateProcessorAsync(); - } - else - { - ClearUpdateProcessor(); - } - } - finally - { - connectionLock.Release(); - } - - return; - } - - void MobileConnectionManager_ConnectionChanged(bool isOnline) - { - Online = isOnline; - } - - /// - public bool BoolVariation(string key, bool defaultValue = false) - { - return VariationWithType(key, defaultValue, JTokenType.Boolean).Value(); - } - - /// - public string StringVariation(string key, string defaultValue) - { - var value = VariationWithType(key, defaultValue, JTokenType.String); - if (value != null) - { - return value.Value(); - } - - return null; - } - - /// - public float FloatVariation(string key, float defaultValue = 0) - { - return VariationWithType(key, defaultValue, JTokenType.Float).Value(); - } - - /// - public int IntVariation(string key, int defaultValue = 0) - { - return VariationWithType(key, defaultValue, JTokenType.Integer).Value(); - } - - /// - public JToken JsonVariation(string key, JToken defaultValue) - { - return VariationWithType(key, defaultValue, null); - } - - JToken VariationWithType(string featureKey, JToken defaultValue, JTokenType? jtokenType) - { - var returnedFlagValue = Variation(featureKey, defaultValue); - if (returnedFlagValue != null && jtokenType != null && !returnedFlagValue.Type.Equals(jtokenType)) - { - Log.ErrorFormat("Expected type: {0} but got {1} when evaluating FeatureFlag: {2}. Returning default", - jtokenType, - returnedFlagValue.Type, - featureKey); - - return defaultValue; - } - - return returnedFlagValue; - } - - JToken Variation(string featureKey, JToken defaultValue) - { - FeatureFlagEvent featureFlagEvent = FeatureFlagEvent.Default(featureKey); - FeatureRequestEvent featureRequestEvent; - - if (!Initialized()) - { - Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); - return defaultValue; - } - - var flag = flagCacheManager.FlagForUser(featureKey, User); - if (flag != null) - { - featureFlagEvent = new FeatureFlagEvent(featureKey, flag); - var value = flag.value; - if (value == null || value.Type == JTokenType.Null) { - featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, - User, - defaultValue); - value = defaultValue; - } else { - featureRequestEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, - User, - flag.variation, - flag.value, - defaultValue); - } - eventProcessor.SendEvent(featureRequestEvent); - return value; - } - - Log.InfoFormat("Unknown feature flag {0}; returning default value", - featureKey); - featureRequestEvent = eventFactory.NewUnknownFeatureRequestEvent(featureKey, - User, - defaultValue); - eventProcessor.SendEvent(featureRequestEvent); - return defaultValue; - } - - /// - public IDictionary AllFlags() - { - if (IsOffline()) - { - Log.Warn("AllFlags() was called when client is in offline mode. Returning null."); - return null; - } - if (!Initialized()) - { - Log.Warn("AllFlags() was called before client has finished initializing. Returning null."); - return null; - } - - return flagCacheManager.FlagsForUser(User) - .ToDictionary(p => p.Key, p => p.Value.value); - } - - /// - public void Track(string eventName, JToken data) - { - eventProcessor.SendEvent(eventFactory.NewCustomEvent(eventName, User, data)); - } - - /// - public void Track(string eventName) - { - Track(eventName, null); - } - - /// - public bool Initialized() - { - //bool isInited = Instance != null; - //return isInited && Online; - // TODO: This method needs to be fixed to actually check whether the update processor has initialized. - // The previous logic (above) was meaningless because this method is not static, so by definition you - // do have a client instance if we've gotten here. But that doesn't mean it is initialized. - return Online; - } - - /// - public bool IsOffline() - { - return !online; - } - - /// - public void Flush() - { - eventProcessor.Flush(); - } - - /// - public void Identify(User user) - { - IdentifyAsync(user).Wait(); - } - - /// - public async Task IdentifyAsync(User user) - { - if (user == null) - { - throw new ArgumentNullException("user"); - } - - User userWithKey = user; - if (String.IsNullOrEmpty(user.Key)) - { - userWithKey = UserWithUniqueKey(user); - } - - await connectionLock.WaitAsync(); - try - { - User = userWithKey; - await RestartUpdateProcessorAsync(); - } - finally - { - connectionLock.Release(); - } - - eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(userWithKey)); - } - - async Task RestartUpdateProcessorAsync() - { - ClearAndSetUpdateProcessor(); - await StartUpdateProcessorAsync(); - } - - void ClearAndSetUpdateProcessor() - { - ClearUpdateProcessor(); - updateProcessor = Factory.CreateUpdateProcessor(Config, User, flagCacheManager); - } - - void ClearUpdateProcessor() - { - if (updateProcessor != null) - { - updateProcessor.Dispose(); - updateProcessor = new NullUpdateProcessor(); - } - } - - User UserWithUniqueKey(User user) - { - string uniqueId = deviceInfo.UniqueDeviceId(); + } + + Instance = new LdClient(configuration, user); + Log.InfoFormat("Initialized LaunchDarkly Client {0}", + Instance.Version); + } + + bool StartUpdateProcessor(TimeSpan maxWaitTime) + { + var initTask = updateProcessor.Start(); + return initTask.Wait(maxWaitTime); + } + + Task StartUpdateProcessorAsync() + { + return updateProcessor.Start(); + } + + void SetupConnectionManager() + { + if (connectionManager is MobileConnectionManager mobileConnectionManager) + { + mobileConnectionManager.ConnectionChanged += MobileConnectionManager_ConnectionChanged; + Log.InfoFormat("The mobile client connection changed online to {0}", + connectionManager.IsConnected); + } + online = connectionManager.IsConnected; + } + + bool online; + /// + public bool Online + { + get => online; + set + { + var doNotAwaitResult = SetOnlineAsync(value); + } + } + + public async Task SetOnlineAsync(bool value) + { + await connectionLock.WaitAsync(); + online = value; + try + { + if (online) + { + await RestartUpdateProcessorAsync(); + } + else + { + ClearUpdateProcessor(); + } + } + finally + { + connectionLock.Release(); + } + + return; + } + + void MobileConnectionManager_ConnectionChanged(bool isOnline) + { + Online = isOnline; + } + + /// + public bool BoolVariation(string key, bool defaultValue = false) + { + return VariationWithType(key, defaultValue, JTokenType.Boolean).Value(); + } + + /// + public string StringVariation(string key, string defaultValue) + { + var value = VariationWithType(key, defaultValue, JTokenType.String); + if (value != null) + { + return value.Value(); + } + + return null; + } + + /// + public float FloatVariation(string key, float defaultValue = 0) + { + return VariationWithType(key, defaultValue, JTokenType.Float).Value(); + } + + /// + public int IntVariation(string key, int defaultValue = 0) + { + return VariationWithType(key, defaultValue, JTokenType.Integer).Value(); + } + + /// + public JToken JsonVariation(string key, JToken defaultValue) + { + return VariationWithType(key, defaultValue, null); + } + + JToken VariationWithType(string featureKey, JToken defaultValue, JTokenType? jtokenType) + { + var returnedFlagValue = Variation(featureKey, defaultValue); + if (returnedFlagValue != null && jtokenType != null && !returnedFlagValue.Type.Equals(jtokenType)) + { + Log.ErrorFormat("Expected type: {0} but got {1} when evaluating FeatureFlag: {2}. Returning default", + jtokenType, + returnedFlagValue.Type, + featureKey); + + return defaultValue; + } + + return returnedFlagValue; + } + + JToken Variation(string featureKey, JToken defaultValue) + { + FeatureFlagEvent featureFlagEvent = FeatureFlagEvent.Default(featureKey); + FeatureRequestEvent featureRequestEvent; + + if (!Initialized()) + { + Log.Warn("LaunchDarkly client has not yet been initialized. Returning default"); + return defaultValue; + } + + var flag = flagCacheManager.FlagForUser(featureKey, User); + if (flag != null) + { + featureFlagEvent = new FeatureFlagEvent(featureKey, flag); + var value = flag.value; + if (value == null || value.Type == JTokenType.Null) { + featureRequestEvent = eventFactory.NewDefaultFeatureRequestEvent(featureFlagEvent, + User, + defaultValue); + value = defaultValue; + } else { + featureRequestEvent = eventFactory.NewFeatureRequestEvent(featureFlagEvent, + User, + flag.variation, + flag.value, + defaultValue); + } + eventProcessor.SendEvent(featureRequestEvent); + return value; + } + + Log.InfoFormat("Unknown feature flag {0}; returning default value", + featureKey); + featureRequestEvent = eventFactory.NewUnknownFeatureRequestEvent(featureKey, + User, + defaultValue); + eventProcessor.SendEvent(featureRequestEvent); + return defaultValue; + } + + /// + public IDictionary AllFlags() + { + if (IsOffline()) + { + Log.Warn("AllFlags() was called when client is in offline mode. Returning null."); + return null; + } + if (!Initialized()) + { + Log.Warn("AllFlags() was called before client has finished initializing. Returning null."); + return null; + } + + return flagCacheManager.FlagsForUser(User) + .ToDictionary(p => p.Key, p => p.Value.value); + } + + /// + public void Track(string eventName, JToken data) + { + eventProcessor.SendEvent(eventFactory.NewCustomEvent(eventName, User, data)); + } + + /// + public void Track(string eventName) + { + Track(eventName, null); + } + + /// + public bool Initialized() + { + //bool isInited = Instance != null; + //return isInited && Online; + // TODO: This method needs to be fixed to actually check whether the update processor has initialized. + // The previous logic (above) was meaningless because this method is not static, so by definition you + // do have a client instance if we've gotten here. But that doesn't mean it is initialized. + return Online; + } + + /// + public bool IsOffline() + { + return !online; + } + + /// + public void Flush() + { + eventProcessor.Flush(); + } + + /// + public void Identify(User user) + { + IdentifyAsync(user).Wait(); + } + + /// + public async Task IdentifyAsync(User user) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + User userWithKey = user; + if (String.IsNullOrEmpty(user.Key)) + { + userWithKey = UserWithUniqueKey(user); + } + + await connectionLock.WaitAsync(); + try + { + User = userWithKey; + await RestartUpdateProcessorAsync(); + } + finally + { + connectionLock.Release(); + } + + eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(userWithKey)); + } + + async Task RestartUpdateProcessorAsync() + { + ClearAndSetUpdateProcessor(); + await StartUpdateProcessorAsync(); + } + + void ClearAndSetUpdateProcessor() + { + ClearUpdateProcessor(); + updateProcessor = Factory.CreateUpdateProcessor(Config, User, flagCacheManager); + } + + void ClearUpdateProcessor() + { + if (updateProcessor != null) + { + updateProcessor.Dispose(); + updateProcessor = new NullUpdateProcessor(); + } + } + + User UserWithUniqueKey(User user) + { + string uniqueId = deviceInfo.UniqueDeviceId(); return new User(user) { Key = uniqueId, Anonymous = true - }; - } - - void IDisposable.Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - void Dispose(bool disposing) - { - if (disposing) - { - Log.InfoFormat("The mobile client is being disposed"); - updateProcessor.Dispose(); - eventProcessor.Dispose(); - } - } - - /// - public Version Version - { - get - { - return MobileClientEnvironment.Instance.Version; - } - } - - /// - public void RegisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) - { - flagListenerManager.RegisterListener(listener, flagKey); - } - - /// - public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) - { - flagListenerManager.UnregisterListener(listener, flagKey); - } - - internal async Task EnterBackgroundAsync() - { - // if using Streaming, processor needs to be reset - if (Config.IsStreamingEnabled) - { - ClearUpdateProcessor(); - Config.IsStreamingEnabled = false; - await RestartUpdateProcessorAsync(); - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); - } - else - { - await PingPollingProcessorAsync(); - } - } - - internal async Task EnterForegroundAsync() - { - ResetProcessorForForeground(); - await RestartUpdateProcessorAsync(); - } - - void ResetProcessorForForeground() - { - string didBackground = persister.GetValue(Constants.BACKGROUNDED_WHILE_STREAMING); - if (didBackground.Equals("true")) - { - persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "false"); - ClearUpdateProcessor(); - Config.IsStreamingEnabled = true; - } - } - - internal void BackgroundTick() - { - PingPollingProcessor(); - } - - internal async Task BackgroundTickAsync() - { - await PingPollingProcessorAsync(); - } - - void PingPollingProcessor() - { - var pollingProcessor = updateProcessor as MobilePollingProcessor; - if (pollingProcessor != null) - { - var waitTask = pollingProcessor.PingAndWait(); - waitTask.Wait(); - } - } - - async Task PingPollingProcessorAsync() - { - var pollingProcessor = updateProcessor as MobilePollingProcessor; - if (pollingProcessor != null) - { - await pollingProcessor.PingAndWait(); - } - } - } + }; + } + + void IDisposable.Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + void Dispose(bool disposing) + { + if (disposing) + { + Log.InfoFormat("The mobile client is being disposed"); + updateProcessor.Dispose(); + eventProcessor.Dispose(); + } + } + + /// + public Version Version + { + get + { + return MobileClientEnvironment.Instance.Version; + } + } + + /// + public void RegisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) + { + flagListenerManager.RegisterListener(listener, flagKey); + } + + /// + public void UnregisterFeatureFlagListener(string flagKey, IFeatureFlagListener listener) + { + flagListenerManager.UnregisterListener(listener, flagKey); + } + + internal async Task EnterBackgroundAsync() + { + // if using Streaming, processor needs to be reset + if (Config.IsStreamingEnabled) + { + ClearUpdateProcessor(); + Config.IsStreamingEnabled = false; + await RestartUpdateProcessorAsync(); + persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); + } + else + { + await PingPollingProcessorAsync(); + } + } + + internal async Task EnterForegroundAsync() + { + ResetProcessorForForeground(); + await RestartUpdateProcessorAsync(); + } + + void ResetProcessorForForeground() + { + string didBackground = persister.GetValue(Constants.BACKGROUNDED_WHILE_STREAMING); + if (didBackground.Equals("true")) + { + persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "false"); + ClearUpdateProcessor(); + Config.IsStreamingEnabled = true; + } + } + + internal void BackgroundTick() + { + PingPollingProcessor(); + } + + internal async Task BackgroundTickAsync() + { + await PingPollingProcessorAsync(); + } + + void PingPollingProcessor() + { + var pollingProcessor = updateProcessor as MobilePollingProcessor; + if (pollingProcessor != null) + { + var waitTask = pollingProcessor.PingAndWait(); + waitTask.Wait(); + } + } + + async Task PingPollingProcessorAsync() + { + var pollingProcessor = updateProcessor as MobilePollingProcessor; + if (pollingProcessor != null) + { + await pollingProcessor.PingAndWait(); + } + } + } } \ No newline at end of file diff --git a/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs index 5e8c58ce..074605e4 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/LdClientEventTests.cs @@ -23,11 +23,9 @@ public void IdentifySendsIdentifyEvent() { User user1 = User.WithKey("userkey1"); client.Identify(user1); - Assert.Collection(eventProcessor.Events, e => - { - IdentifyEvent ie = Assert.IsType(e); - Assert.Equal(user1.Key, ie.User.Key); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), // there's always an initial identify event + e => CheckIdentifyEvent(e, user1)); } } @@ -38,13 +36,14 @@ public void TrackSendsCustomEvent() { JToken data = new JValue("hi"); client.Track("eventkey", data); - Assert.Collection(eventProcessor.Events, e => - { - CustomEvent ce = Assert.IsType(e); - Assert.Equal("eventkey", ce.Key); - Assert.Equal(user.Key, ce.User.Key); - Assert.Equal(data, ce.JsonData); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + CustomEvent ce = Assert.IsType(e); + Assert.Equal("eventkey", ce.Key); + Assert.Equal(user.Key, ce.User.Key); + Assert.Equal(data, ce.JsonData); + }); } } @@ -58,17 +57,18 @@ public void VariationSendsFeatureEventForValidFlag() { string result = client.StringVariation("flag", "b"); Assert.Equal("a", result); - Assert.Collection(eventProcessor.Events, e => - { - FeatureRequestEvent fe = Assert.IsType(e); - Assert.Equal("flag", fe.Key); - Assert.Equal("a", fe.Value); - Assert.Equal(1, fe.Variation); - Assert.Equal(1000, fe.Version); - Assert.Equal("b", fe.Default); - Assert.True(fe.TrackEvents); - Assert.Equal(2000, fe.DebugEventsUntilDate); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("a", fe.Value); + Assert.Equal(1, fe.Variation); + Assert.Equal(1000, fe.Version); + Assert.Equal("b", fe.Default); + Assert.True(fe.TrackEvents); + Assert.Equal(2000, fe.DebugEventsUntilDate); + }); } } @@ -82,15 +82,16 @@ public void FeatureEventUsesFlagVersionIfProvided() { string result = client.StringVariation("flag", "b"); Assert.Equal("a", result); - Assert.Collection(eventProcessor.Events, e => - { - FeatureRequestEvent fe = Assert.IsType(e); - Assert.Equal("flag", fe.Key); - Assert.Equal("a", fe.Value); - Assert.Equal(1, fe.Variation); - Assert.Equal(1500, fe.Version); - Assert.Equal("b", fe.Default); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("a", fe.Value); + Assert.Equal(1, fe.Variation); + Assert.Equal(1500, fe.Version); + Assert.Equal("b", fe.Default); + }); } } @@ -103,15 +104,16 @@ public void VariationSendsFeatureEventForDefaultValue() { string result = client.StringVariation("flag", "b"); Assert.Equal("b", result); - Assert.Collection(eventProcessor.Events, e => - { - FeatureRequestEvent fe = Assert.IsType(e); - Assert.Equal("flag", fe.Key); - Assert.Equal("b", fe.Value); - Assert.Null(fe.Variation); - Assert.Equal(1000, fe.Version); - Assert.Equal("b", fe.Default); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("b", fe.Value); + Assert.Null(fe.Variation); + Assert.Equal(1000, fe.Version); + Assert.Equal("b", fe.Default); + }); } } @@ -122,16 +124,23 @@ public void VariationSendsFeatureEventForUnknownFlag() { string result = client.StringVariation("flag", "b"); Assert.Equal("b", result); - Assert.Collection(eventProcessor.Events, e => - { - FeatureRequestEvent fe = Assert.IsType(e); - Assert.Equal("flag", fe.Key); - Assert.Equal("b", fe.Value); - Assert.Null(fe.Variation); - Assert.Null(fe.Version); - Assert.Equal("b", fe.Default); - }); + Assert.Collection(eventProcessor.Events, + e => CheckIdentifyEvent(e, user), + e => { + FeatureRequestEvent fe = Assert.IsType(e); + Assert.Equal("flag", fe.Key); + Assert.Equal("b", fe.Value); + Assert.Null(fe.Variation); + Assert.Null(fe.Version); + Assert.Equal("b", fe.Default); + }); } } + + private void CheckIdentifyEvent(Event e, User u) + { + IdentifyEvent ie = Assert.IsType(e); + Assert.Equal(u.Key, ie.User.Key); + } } } From 3f2d0c090fd0c68cbd853fc66261be93474f274c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 24 Jul 2018 17:06:18 -0700 Subject: [PATCH 31/47] misc project file & test cleanup --- .../LaunchDarkly.Xamarin.csproj | 24 +- .../MobileStreamingProcessorTests.cs | 414 +++++++++--------- 2 files changed, 223 insertions(+), 215 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 6b163c85..2d7142ff 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -2,25 +2,31 @@ 1.0.0-beta11 - netstandard1.6;netstandard2.0;net45 + Library + LaunchDarkly.Xamarin + LaunchDarkly.Xamarin - - - false + + + netstandard1.6;netstandard2.0;net45 - - obj\Release\netstandard2.0 - - + + netstandard1.6;netstandard2.0 + - + + + + + + diff --git a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs index 83f99c41..fcb0e08e 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/MobileStreamingProcessorTests.cs @@ -1,211 +1,213 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using LaunchDarkly.Client; -using LaunchDarkly.Common; -using LaunchDarkly.EventSource; -using Newtonsoft.Json; -using Xunit; - -namespace LaunchDarkly.Xamarin.Tests -{ - public class MobileStreamingProcessorTests - { +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LaunchDarkly.Client; +using LaunchDarkly.Common; +using LaunchDarkly.EventSource; +using Newtonsoft.Json; +using Xunit; + +namespace LaunchDarkly.Xamarin.Tests +{ + public class MobileStreamingProcessorTests + { private const string initialFlagsJson = "{" + "\"int-flag\":{\"value\":15,\"version\":100}," + "\"float-flag\":{\"value\":13.5,\"version\":100}," + "\"string-flag\":{\"value\":\"markw@magenic.com\",\"version\":100}" + "}"; - - User user = User.WithKey("user key"); - EventSourceMock mockEventSource; - TestEventSourceFactory eventSourceFactory; - IFlagCacheManager mockFlagCacheMgr; - - private IMobileUpdateProcessor MobileStreamingProcessorStarted() - { - mockEventSource = new EventSourceMock(); - eventSourceFactory = new TestEventSourceFactory(mockEventSource); - // stub with an empty InMemoryCache, so Stream updates can be tested - mockFlagCacheMgr = new MockFlagCacheManager(new UserFlagInMemoryCache()); - var config = Configuration.Default("someKey") - .WithConnectionManager(new MockConnectionManager(true)) - .WithIsStreamingEnabled(true) - .WithFlagCacheManager(mockFlagCacheMgr); - - var processor = Factory.CreateUpdateProcessor(config, user, mockFlagCacheMgr, eventSourceFactory.Create()); - processor.Start(); - return processor; - } - - [Fact] - public void CanCreateMobileStreamingProcFromFactory() - { - var streamingProcessor = MobileStreamingProcessorStarted(); - Assert.IsType(streamingProcessor); - } - - [Fact] - public void PUTstoresFeatureFlags() - { - var streamingProcessor = MobileStreamingProcessorStarted(); - // should be empty before PUT message arrives - var flagsInCache = mockFlagCacheMgr.FlagsForUser(user); - Assert.Empty(flagsInCache); - - PUTMessageSentToProcessor(); - flagsInCache = mockFlagCacheMgr.FlagsForUser(user); - Assert.NotEmpty(flagsInCache); - int intFlagValue = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, intFlagValue); - } - - [Fact] - public void PATCHupdatesFeatureFlag() - { - // before PATCH, fill in flags - var streamingProcessor = MobileStreamingProcessorStarted(); - PUTMessageSentToProcessor(); - var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, intFlagFromPUT); - - //PATCH to update 1 flag - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(UpdatedFlag(), null), "patch"); - mockEventSource.RaiseMessageRcvd(eventArgs); - - //verify flag has changed - int flagFromPatch = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(99, flagFromPatch); - } - - [Fact] - public void PATCHdoesnotUpdateFlagIfVersionIsLower() - { - // before PATCH, fill in flags - var streamingProcessor = MobileStreamingProcessorStarted(); - PUTMessageSentToProcessor(); - var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, intFlagFromPUT); - - //PATCH to update 1 flag - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(UpdatedFlagWithLowerVersion(), null), "patch"); - mockEventSource.RaiseMessageRcvd(eventArgs); - - //verify flag has not changed - int flagFromPatch = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, flagFromPatch); - } - - [Fact] - public void DELETEremovesFeatureFlag() - { - // before DELETE, fill in flags, test it's there - var streamingProcessor = MobileStreamingProcessorStarted(); - PUTMessageSentToProcessor(); - var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, intFlagFromPUT); - - // DELETE int-flag - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(DeleteFlag(), null), "delete"); - mockEventSource.RaiseMessageRcvd(eventArgs); - - // verify flag was deleted - Assert.Null(mockFlagCacheMgr.FlagForUser("int-flag", user)); - } - - [Fact] - public void DELTEdoesnotRemoveFeatureFlagIfVersionIsLower() - { - // before DELETE, fill in flags, test it's there - var streamingProcessor = MobileStreamingProcessorStarted(); - PUTMessageSentToProcessor(); - var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); - Assert.Equal(15, intFlagFromPUT); - - // DELETE int-flag - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(DeleteFlagWithLowerVersion(), null), "delete"); - mockEventSource.RaiseMessageRcvd(eventArgs); - - // verify flag was not deleted - Assert.NotNull(mockFlagCacheMgr.FlagForUser("int-flag", user)); - } - - string UpdatedFlag() - { - var updatedFlagAsJson = "{\"key\":\"int-flag\",\"version\":999,\"flagVersion\":192,\"value\":99,\"variation\":0,\"trackEvents\":false}"; - return updatedFlagAsJson; - } - - string DeleteFlag() - { - var flagToDelete = "{\"key\":\"int-flag\",\"version\":1214}"; - return flagToDelete; - } - - string UpdatedFlagWithLowerVersion() - { - var updatedFlagAsJson = "{\"key\":\"int-flag\",\"version\":1,\"flagVersion\":192,\"value\":99,\"variation\":0,\"trackEvents\":false}"; - return updatedFlagAsJson; - } - - string DeleteFlagWithLowerVersion() - { - var flagToDelete = "{\"key\":\"int-flag\",\"version\":1}"; - return flagToDelete; - } - - void PUTMessageSentToProcessor() - { - MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(initialFlagsJson, null), "put"); - mockEventSource.RaiseMessageRcvd(eventArgs); - } - } - - class TestEventSourceFactory - { - public StreamProperties ReceivedProperties { get; private set; } - public IDictionary ReceivedHeaders { get; private set; } - IEventSource _eventSource; - - public TestEventSourceFactory(IEventSource eventSource) - { - _eventSource = eventSource; - } - - public StreamManager.EventSourceCreator Create() - { - return (StreamProperties sp, IDictionary headers) => - { - ReceivedProperties = sp; - ReceivedHeaders = headers; - return _eventSource; - }; - } - } - - class EventSourceMock : IEventSource - { - public ReadyState ReadyState => throw new NotImplementedException(); - - public event EventHandler Opened; - public event EventHandler Closed; - public event EventHandler MessageReceived; - public event EventHandler CommentReceived; - public event EventHandler Error; - - public void Close() - { - - } - - public Task StartAsync() - { - return Task.CompletedTask; - } - - public void RaiseMessageRcvd(MessageReceivedEventArgs eventArgs) - { - MessageReceived(null, eventArgs); - } - } -} + + User user = User.WithKey("user key"); + EventSourceMock mockEventSource; + TestEventSourceFactory eventSourceFactory; + IFlagCacheManager mockFlagCacheMgr; + + private IMobileUpdateProcessor MobileStreamingProcessorStarted() + { + mockEventSource = new EventSourceMock(); + eventSourceFactory = new TestEventSourceFactory(mockEventSource); + // stub with an empty InMemoryCache, so Stream updates can be tested + mockFlagCacheMgr = new MockFlagCacheManager(new UserFlagInMemoryCache()); + var config = Configuration.Default("someKey") + .WithConnectionManager(new MockConnectionManager(true)) + .WithIsStreamingEnabled(true) + .WithFlagCacheManager(mockFlagCacheMgr); + + var processor = Factory.CreateUpdateProcessor(config, user, mockFlagCacheMgr, eventSourceFactory.Create()); + processor.Start(); + return processor; + } + + [Fact] + public void CanCreateMobileStreamingProcFromFactory() + { + var streamingProcessor = MobileStreamingProcessorStarted(); + Assert.IsType(streamingProcessor); + } + + [Fact] + public void PUTstoresFeatureFlags() + { + var streamingProcessor = MobileStreamingProcessorStarted(); + // should be empty before PUT message arrives + var flagsInCache = mockFlagCacheMgr.FlagsForUser(user); + Assert.Empty(flagsInCache); + + PUTMessageSentToProcessor(); + flagsInCache = mockFlagCacheMgr.FlagsForUser(user); + Assert.NotEmpty(flagsInCache); + int intFlagValue = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, intFlagValue); + } + + [Fact] + public void PATCHupdatesFeatureFlag() + { + // before PATCH, fill in flags + var streamingProcessor = MobileStreamingProcessorStarted(); + PUTMessageSentToProcessor(); + var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, intFlagFromPUT); + + //PATCH to update 1 flag + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(UpdatedFlag(), null), "patch"); + mockEventSource.RaiseMessageRcvd(eventArgs); + + //verify flag has changed + int flagFromPatch = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(99, flagFromPatch); + } + + [Fact] + public void PATCHdoesnotUpdateFlagIfVersionIsLower() + { + // before PATCH, fill in flags + var streamingProcessor = MobileStreamingProcessorStarted(); + PUTMessageSentToProcessor(); + var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, intFlagFromPUT); + + //PATCH to update 1 flag + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(UpdatedFlagWithLowerVersion(), null), "patch"); + mockEventSource.RaiseMessageRcvd(eventArgs); + + //verify flag has not changed + int flagFromPatch = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, flagFromPatch); + } + + [Fact] + public void DELETEremovesFeatureFlag() + { + // before DELETE, fill in flags, test it's there + var streamingProcessor = MobileStreamingProcessorStarted(); + PUTMessageSentToProcessor(); + var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, intFlagFromPUT); + + // DELETE int-flag + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(DeleteFlag(), null), "delete"); + mockEventSource.RaiseMessageRcvd(eventArgs); + + // verify flag was deleted + Assert.Null(mockFlagCacheMgr.FlagForUser("int-flag", user)); + } + + [Fact] + public void DELTEdoesnotRemoveFeatureFlagIfVersionIsLower() + { + // before DELETE, fill in flags, test it's there + var streamingProcessor = MobileStreamingProcessorStarted(); + PUTMessageSentToProcessor(); + var intFlagFromPUT = mockFlagCacheMgr.FlagForUser("int-flag", user).value.ToObject(); + Assert.Equal(15, intFlagFromPUT); + + // DELETE int-flag + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(DeleteFlagWithLowerVersion(), null), "delete"); + mockEventSource.RaiseMessageRcvd(eventArgs); + + // verify flag was not deleted + Assert.NotNull(mockFlagCacheMgr.FlagForUser("int-flag", user)); + } + + string UpdatedFlag() + { + var updatedFlagAsJson = "{\"key\":\"int-flag\",\"version\":999,\"flagVersion\":192,\"value\":99,\"variation\":0,\"trackEvents\":false}"; + return updatedFlagAsJson; + } + + string DeleteFlag() + { + var flagToDelete = "{\"key\":\"int-flag\",\"version\":1214}"; + return flagToDelete; + } + + string UpdatedFlagWithLowerVersion() + { + var updatedFlagAsJson = "{\"key\":\"int-flag\",\"version\":1,\"flagVersion\":192,\"value\":99,\"variation\":0,\"trackEvents\":false}"; + return updatedFlagAsJson; + } + + string DeleteFlagWithLowerVersion() + { + var flagToDelete = "{\"key\":\"int-flag\",\"version\":1}"; + return flagToDelete; + } + + void PUTMessageSentToProcessor() + { + MessageReceivedEventArgs eventArgs = new MessageReceivedEventArgs(new MessageEvent(initialFlagsJson, null), "put"); + mockEventSource.RaiseMessageRcvd(eventArgs); + } + } + + class TestEventSourceFactory + { + public StreamProperties ReceivedProperties { get; private set; } + public IDictionary ReceivedHeaders { get; private set; } + IEventSource _eventSource; + + public TestEventSourceFactory(IEventSource eventSource) + { + _eventSource = eventSource; + } + + public StreamManager.EventSourceCreator Create() + { + return (StreamProperties sp, IDictionary headers) => + { + ReceivedProperties = sp; + ReceivedHeaders = headers; + return _eventSource; + }; + } + } + + class EventSourceMock : IEventSource + { + public ReadyState ReadyState => throw new NotImplementedException(); + +#pragma warning disable 0067 // unused properties + public event EventHandler Opened; + public event EventHandler Closed; + public event EventHandler MessageReceived; + public event EventHandler CommentReceived; + public event EventHandler Error; +#pragma warning restore 0067 + + public void Close() + { + + } + + public Task StartAsync() + { + return Task.CompletedTask; + } + + public void RaiseMessageRcvd(MessageReceivedEventArgs eventArgs) + { + MessageReceived(null, eventArgs); + } + } +} From a25b8cc2bfd6d17470a7b29fa1d7ef3188e43da1 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 26 Jul 2018 19:42:01 -0700 Subject: [PATCH 32/47] beta12 release (fixes System.Runtime reference problem in LD.Common) --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 4 ++-- .../LaunchDarkly.Xamarin.Tests.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 2d7142ff..1d45d321 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,7 +1,7 @@ - 1.0.0-beta11 + 1.0.0-beta12 Library LaunchDarkly.Xamarin LaunchDarkly.Xamarin @@ -18,7 +18,7 @@ - + diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index aa036d1b..0b13954b 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -9,7 +9,7 @@ - + From 17b758586549c062cf85999cbbd1651cdac59cb8 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Jul 2018 15:36:18 -0700 Subject: [PATCH 33/47] provide callback mechanism for backgrounding --- src/LaunchDarkly.Xamarin/Configuration.cs | 13 +++++ src/LaunchDarkly.Xamarin/Factory.cs | 52 ++++++------------- .../IBackgroundingState.cs | 33 ++++++++++++ src/LaunchDarkly.Xamarin/IPlatformAdapter.cs | 37 +++++++++++++ src/LaunchDarkly.Xamarin/LdClient.cs | 36 ++++++++++++- 5 files changed, 134 insertions(+), 37 deletions(-) create mode 100644 src/LaunchDarkly.Xamarin/IBackgroundingState.cs create mode 100644 src/LaunchDarkly.Xamarin/IPlatformAdapter.cs diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index 4910fe1f..a6bdda2f 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -125,6 +125,7 @@ public class Configuration : IMobileConfiguration internal ISimplePersistance Persister { get; set; } internal IDeviceInfo DeviceInfo { get; set; } internal IFeatureFlagListenerManager FeatureFlagListenerManager { get; set; } + internal IPlatformAdapter PlatformAdapter { get; set; } /// /// Default value for . @@ -651,5 +652,17 @@ public static Configuration WithBackgroundPollingInterval(this Configuration con configuration.BackgroundPollingInterval = backgroundPollingInternal; return configuration; } + + /// + /// Specifies a component that provides special functionality for the current mobile platform. + /// + /// Configuration. + /// An implementation of . + /// the same Configuration instance + public static Configuration WithPlatformAdapter(this Configuration configuration, IPlatformAdapter adapter) + { + configuration.PlatformAdapter = adapter; + return configuration; + } } } diff --git a/src/LaunchDarkly.Xamarin/Factory.cs b/src/LaunchDarkly.Xamarin/Factory.cs index 5fb139c0..fa082f67 100644 --- a/src/LaunchDarkly.Xamarin/Factory.cs +++ b/src/LaunchDarkly.Xamarin/Factory.cs @@ -14,27 +14,21 @@ internal static IFlagCacheManager CreateFlagCacheManager(Configuration configura IFlagListenerUpdater updater, User user) { - IFlagCacheManager flagCacheManager; - if (configuration.FlagCacheManager != null) { - flagCacheManager = configuration.FlagCacheManager; + return configuration.FlagCacheManager; } else { var inMemoryCache = new UserFlagInMemoryCache(); var deviceCache = new UserFlagDeviceCache(persister); - flagCacheManager = new FlagCacheManager(inMemoryCache, deviceCache, updater, user); + return new FlagCacheManager(inMemoryCache, deviceCache, updater, user); } - - return flagCacheManager; } internal static IConnectionManager CreateConnectionManager(Configuration configuration) { - IConnectionManager connectionManager; - connectionManager = configuration.ConnectionManager ?? new MobileConnectionManager(); - return connectionManager; + return configuration.ConnectionManager ?? new MobileConnectionManager(); } internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration configuration, @@ -47,29 +41,26 @@ internal static IMobileUpdateProcessor CreateUpdateProcessor(Configuration confi return configuration.MobileUpdateProcessor; } - IMobileUpdateProcessor updateProcessor = null; if (configuration.Offline) { - Log.InfoFormat("Was configured to be offline, starting service with NullUpdateProcessor"); + Log.InfoFormat("Starting LaunchDarkly client in offline mode"); return new NullUpdateProcessor(); } if (configuration.IsStreamingEnabled) { - updateProcessor = new MobileStreamingProcessor(configuration, + return new MobileStreamingProcessor(configuration, flagCacheManager, user, source); } else { var featureFlagRequestor = new FeatureFlagRequestor(configuration, user); - updateProcessor = new MobilePollingProcessor(featureFlagRequestor, - flagCacheManager, - user, - configuration.PollingInterval); + return new MobilePollingProcessor(featureFlagRequestor, + flagCacheManager, + user, + configuration.PollingInterval); } - - return updateProcessor; } internal static IEventProcessor CreateEventProcessor(Configuration configuration) @@ -80,7 +71,6 @@ internal static IEventProcessor CreateEventProcessor(Configuration configuration } if (configuration.Offline) { - Log.InfoFormat("Was configured to be offline, starting service with NullEventProcessor"); return new NullEventProcessor(); } @@ -90,32 +80,22 @@ internal static IEventProcessor CreateEventProcessor(Configuration configuration internal static ISimplePersistance CreatePersister(Configuration configuration) { - if (configuration.Persister != null) - { - return configuration.Persister; - } - - return new SimpleMobileDevicePersistance(); + return configuration.Persister ?? new SimpleMobileDevicePersistance(); } internal static IDeviceInfo CreateDeviceInfo(Configuration configuration) { - if (configuration.DeviceInfo != null) - { - return configuration.DeviceInfo; - } - - return new DeviceInfo(); + return configuration.DeviceInfo ?? new DeviceInfo(); } internal static IFeatureFlagListenerManager CreateFeatureFlagListenerManager(Configuration configuration) { - if (configuration.FeatureFlagListenerManager != null) - { - return configuration.FeatureFlagListenerManager; - } + return configuration.FeatureFlagListenerManager ?? new FeatureFlagListenerManager(); + } - return new FeatureFlagListenerManager(); + internal static IPlatformAdapter CreatePlatformAdapter(Configuration configuration) + { + return configuration.PlatformAdapter ?? new NullPlatformAdapter(); } } } diff --git a/src/LaunchDarkly.Xamarin/IBackgroundingState.cs b/src/LaunchDarkly.Xamarin/IBackgroundingState.cs new file mode 100644 index 00000000..019c2fbd --- /dev/null +++ b/src/LaunchDarkly.Xamarin/IBackgroundingState.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace LaunchDarkly.Xamarin +{ + /// + /// An interface that is used internally by implementations of + /// to update the state of the LaunchDarkly client in background mode. Application code does not need + /// to interact with this interface. + /// + public interface IBackgroundingState + { + /// + /// Tells the LaunchDarkly client that the application is entering background mode. The client will + /// suspend the regular streaming or polling process, except when + /// is called. + /// + Task EnterBackgroundAsync(); + + /// + /// Tells the LaunchDarkly client that the application is exiting background mode. The client will + /// resume the regular streaming or polling process. + /// + Task ExitBackgroundAsync(); + + /// + /// Tells the LaunchDarkly client to initiate a request for feature flag updates while in background mode. + /// + Task BackgroundUpdateAsync(); + } +} diff --git a/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs new file mode 100644 index 00000000..ad49c2c1 --- /dev/null +++ b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace LaunchDarkly.Xamarin +{ + /// + /// Interface for a component that helps LdClient interact with a specific mobile platform. + /// Currently this is necessary in order to handle features that are not part of the portable + /// Xamarin.Essentials API; in the future it may be handled automatically when you + /// create an LdClient. + /// + /// To obtain an instance of this interface, use the implementation of `PlatformComponents.CreatePlatformAdapter` + /// that is provided by the add-on library for your specific platform (e.g. LaunchDarkly.Xamarin.Android). + /// Then pass the object to + /// when you are building your client configuration. + /// + /// Application code should not call any methods of this interface directly; they are used internally + /// by LdClient. + /// + public interface IPlatformAdapter : IDisposable + { + /// + /// Tells the IPlatformAdapter to start monitoring the foreground/background state of + /// the application, and provides a callback object for it to use when the state changes. + /// + /// An implementation of IBackgroundingState provided by the client + void EnableBackgrounding(IBackgroundingState backgroundingState); + } + + internal class NullPlatformAdapter : IPlatformAdapter + { + public void EnableBackgrounding(IBackgroundingState backgroundingState) { } + + public void Dispose() { } + } +} diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 9c74e5f1..e7636eef 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -50,6 +50,7 @@ public sealed class LdClient : ILdMobileClient IDeviceInfo deviceInfo; EventFactory eventFactory = EventFactory.Default; IFeatureFlagListenerManager flagListenerManager; + IPlatformAdapter platformAdapter; SemaphoreSlim connectionLock; @@ -75,6 +76,7 @@ public sealed class LdClient : ILdMobileClient persister = Factory.CreatePersister(configuration); deviceInfo = Factory.CreateDeviceInfo(configuration); flagListenerManager = Factory.CreateFeatureFlagListenerManager(configuration); + platformAdapter = Factory.CreatePlatformAdapter(configuration); // If you pass in a user with a null or blank key, one will be assigned to them. if (String.IsNullOrEmpty(user.Key)) @@ -222,6 +224,11 @@ static void CreateInstance(Configuration configuration, User user) Instance = new LdClient(configuration, user); Log.InfoFormat("Initialized LaunchDarkly Client {0}", Instance.Version); + + if (configuration.EnableBackgroundUpdating) + { + Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance)); + } } bool StartUpdateProcessor(TimeSpan maxWaitTime) @@ -520,7 +527,8 @@ void Dispose(bool disposing) { if (disposing) { - Log.InfoFormat("The mobile client is being disposed"); + Log.InfoFormat("Shutting down the LaunchDarkly client"); + platformAdapter.Dispose(); updateProcessor.Dispose(); eventProcessor.Dispose(); } @@ -618,4 +626,30 @@ private Exception UnwrapAggregateException(AggregateException e) return e; } } + + // Implementation of IBackgroundingState - this allows us to keep these methods out of the public LdClient API + internal class LdClientBackgroundingState : IBackgroundingState + { + private readonly LdClient _client; + + internal LdClientBackgroundingState(LdClient client) + { + _client = client; + } + + public async Task EnterBackgroundAsync() + { + await _client.EnterBackgroundAsync(); + } + + public async Task ExitBackgroundAsync() + { + await _client.EnterForegroundAsync(); + } + + public async Task BackgroundUpdateAsync() + { + await _client.BackgroundTickAsync(); + } + } } \ No newline at end of file From d7bc235f023dd0c8f97540075ddb71bda1c560c9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Jul 2018 15:48:20 -0700 Subject: [PATCH 34/47] don't try to restart the update processor in background unless backgrounding is enabled --- src/LaunchDarkly.Xamarin/LdClient.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index e7636eef..6213b2c0 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -562,12 +562,18 @@ internal async Task EnterBackgroundAsync() { ClearUpdateProcessor(); Config.IsStreamingEnabled = false; - await RestartUpdateProcessorAsync(); + if (Config.EnableBackgroundUpdating) + { + await RestartUpdateProcessorAsync(); + } persister.Save(Constants.BACKGROUNDED_WHILE_STREAMING, "true"); } else { - await PingPollingProcessorAsync(); + if (Config.EnableBackgroundUpdating) + { + await PingPollingProcessorAsync(); + } } } From 7c023e24ea5028de522dbfeec3b0efb769a20442 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Jul 2018 15:50:05 -0700 Subject: [PATCH 35/47] always initialize platform adapter --- src/LaunchDarkly.Xamarin/IPlatformAdapter.cs | 5 +++-- src/LaunchDarkly.Xamarin/LdClient.cs | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs index ad49c2c1..278d3491 100644 --- a/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs +++ b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs @@ -25,12 +25,13 @@ public interface IPlatformAdapter : IDisposable /// the application, and provides a callback object for it to use when the state changes. /// /// An implementation of IBackgroundingState provided by the client - void EnableBackgrounding(IBackgroundingState backgroundingState); + /// True if the client should poll while in the background + void EnableBackgrounding(IBackgroundingState backgroundingState, bool pollWhileInBackground); } internal class NullPlatformAdapter : IPlatformAdapter { - public void EnableBackgrounding(IBackgroundingState backgroundingState) { } + public void EnableBackgrounding(IBackgroundingState backgroundingState, bool pollWhileInBackground) { } public void Dispose() { } } diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 6213b2c0..b42ef6eb 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -225,10 +225,8 @@ static void CreateInstance(Configuration configuration, User user) Log.InfoFormat("Initialized LaunchDarkly Client {0}", Instance.Version); - if (configuration.EnableBackgroundUpdating) - { - Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance)); - } + Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance), + configuration.EnableBackgroundUpdating); } bool StartUpdateProcessor(TimeSpan maxWaitTime) From a7376c05c78e3867463d5a089b9a698cd1f46b25 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Jul 2018 17:25:46 -0700 Subject: [PATCH 36/47] doc comment typo --- src/LaunchDarkly.Xamarin/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 9c74e5f1..8f02f24b 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -129,7 +129,7 @@ public static LdClient Init(string mobileKey, User user, TimeSpan maxWaitTime) /// from the LaunchDarkly service. /// /// This is the creation point for LdClient, you must use this static method or the more specific - /// to instantiate the single instance of LdClient + /// to instantiate the single instance of LdClient /// for the lifetime of your application. /// /// The singleton LdClient instance. From 0de1c94dd6519e4d9fdc3c8e1b73324b5f537331 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Jul 2018 17:26:09 -0700 Subject: [PATCH 37/47] doc comment typo --- src/LaunchDarkly.Xamarin/LdClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 8f02f24b..cccc9157 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -152,7 +152,7 @@ public static async Task InitAsync(string mobileKey, User user) /// If you would rather this happen in an async fashion you can use . /// /// This is the creation point for LdClient, you must use this static method or the more basic - /// to instantiate the single instance of LdClient + /// to instantiate the single instance of LdClient /// for the lifetime of your application. /// /// The singleton LdClient instance. From a97dd0d3763a714b2968a74ab981e2c922c14862 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Jul 2018 10:15:58 -0700 Subject: [PATCH 38/47] fix hang in synchronous Identify --- src/LaunchDarkly.Xamarin/LdClient.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index cccc9157..5996f6b0 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -443,7 +443,10 @@ public void Identify(User user) { try { - IdentifyAsync(user).Wait(); + // Note that we must use Task.Run here, rather than just doing IdentifyAsync(user).Wait(), + // to avoid a deadlock if we are on the main thread. See: + // https://olitee.com/2015/01/c-async-await-common-deadlock-scenario/ + Task.Run(() => IdentifyAsync(user)).Wait(); } catch (AggregateException e) { @@ -454,6 +457,8 @@ public void Identify(User user) /// public async Task IdentifyAsync(User user) { + Log.Warn("IdentifyAsync"); + if (user == null) { throw new ArgumentNullException("user"); @@ -477,6 +482,8 @@ public async Task IdentifyAsync(User user) } eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(userWithKey)); + + Log.Warn("IdentifyAsync ending"); } async Task RestartUpdateProcessorAsync() From d631156db35a6f15104fac318892db8c40fffd9b Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Jul 2018 10:16:18 -0700 Subject: [PATCH 39/47] rm debugging --- src/LaunchDarkly.Xamarin/LdClient.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index 5996f6b0..79bccf3f 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -457,8 +457,6 @@ public void Identify(User user) /// public async Task IdentifyAsync(User user) { - Log.Warn("IdentifyAsync"); - if (user == null) { throw new ArgumentNullException("user"); @@ -482,8 +480,6 @@ public async Task IdentifyAsync(User user) } eventProcessor.SendEvent(eventFactory.NewIdentifyEvent(userWithKey)); - - Log.Warn("IdentifyAsync ending"); } async Task RestartUpdateProcessorAsync() From b8da2e04ceb1ed7ee8574bcd67d88fc2555707f9 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Jul 2018 11:20:01 -0700 Subject: [PATCH 40/47] need to pass polling interval to adapter --- src/LaunchDarkly.Xamarin/IPlatformAdapter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs index 278d3491..a50c924e 100644 --- a/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs +++ b/src/LaunchDarkly.Xamarin/IPlatformAdapter.cs @@ -25,13 +25,13 @@ public interface IPlatformAdapter : IDisposable /// the application, and provides a callback object for it to use when the state changes. /// /// An implementation of IBackgroundingState provided by the client - /// True if the client should poll while in the background - void EnableBackgrounding(IBackgroundingState backgroundingState, bool pollWhileInBackground); + /// if non-null, the interval at which polling should happen in the background + void EnableBackgrounding(IBackgroundingState backgroundingState, TimeSpan? backgroundPollInterval); } internal class NullPlatformAdapter : IPlatformAdapter { - public void EnableBackgrounding(IBackgroundingState backgroundingState, bool pollWhileInBackground) { } + public void EnableBackgrounding(IBackgroundingState backgroundingState, TimeSpan? backgroundPollInterval) { } public void Dispose() { } } From bb7638049f462a567dbb4664e2cc2ba5c7f1650e Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Jul 2018 11:56:43 -0700 Subject: [PATCH 41/47] fix method parameter --- src/LaunchDarkly.Xamarin/LdClient.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LdClient.cs b/src/LaunchDarkly.Xamarin/LdClient.cs index e943dc73..188f598a 100644 --- a/src/LaunchDarkly.Xamarin/LdClient.cs +++ b/src/LaunchDarkly.Xamarin/LdClient.cs @@ -225,8 +225,12 @@ static void CreateInstance(Configuration configuration, User user) Log.InfoFormat("Initialized LaunchDarkly Client {0}", Instance.Version); - Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance), - configuration.EnableBackgroundUpdating); + TimeSpan? bgPollInterval = null; + if (configuration.EnableBackgroundUpdating) + { + bgPollInterval = configuration.BackgroundPollingInterval; + } + Instance.platformAdapter.EnableBackgrounding(new LdClientBackgroundingState(Instance), bgPollInterval); } bool StartUpdateProcessor(TimeSpan maxWaitTime) From 3342490965d6d4708d0d4ce5e54bb4567d733b6a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Jul 2018 13:43:49 -0700 Subject: [PATCH 42/47] version 1.0.0-beta13 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 1d45d321..51d35c2d 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,7 +1,7 @@ - 1.0.0-beta12 + 1.0.0-beta13 Library LaunchDarkly.Xamarin LaunchDarkly.Xamarin From 7a0340961d8a4008234b084a988fc05785437ead Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 31 Jul 2018 11:08:33 -0700 Subject: [PATCH 43/47] don't allow null/empty mobile key --- src/LaunchDarkly.Xamarin/Configuration.cs | 4 ++++ .../LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index a6bdda2f..dc6e578c 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -188,6 +188,10 @@ public class Configuration : IMobileConfiguration /// a Configuration instance public static Configuration Default(string mobileKey) { + if (String.IsNullOrEmpty(mobileKey)) + { + throw new ArgumentOutOfRangeException("mobileKey", "key is required"); + } var defaultConfiguration = new Configuration { BaseUri = DefaultUri, diff --git a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs index 250deb68..af1c0fc7 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs @@ -36,6 +36,18 @@ public void CanOverrideStreamConfiguration() Assert.Equal(TimeSpan.FromDays(1), config.ReconnectTime); } + [Fact] + public void MobileKeyCannotBeNull() + { + Assert.Throws(() => Configuration.Default(null)); + } + + [Fact] + public void MobileKeyCannotBeEmpty() + { + Assert.Throws(() => Configuration.Default("")); + } + [Fact] public void CannotOverrideTooSmallPollingInterval() { From f73aaa65ef4fc5a29bf909425c17c84c4f7a1708 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 31 Jul 2018 12:48:39 -0700 Subject: [PATCH 44/47] make polling intervals consistent with other mobile SDKs --- src/LaunchDarkly.Xamarin/Configuration.cs | 23 +++++++++++++++---- .../ConfigurationTest.cs | 18 +++++++++++---- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/Configuration.cs b/src/LaunchDarkly.Xamarin/Configuration.cs index dc6e578c..0435e21e 100644 --- a/src/LaunchDarkly.Xamarin/Configuration.cs +++ b/src/LaunchDarkly.Xamarin/Configuration.cs @@ -130,7 +130,11 @@ public class Configuration : IMobileConfiguration /// /// Default value for . /// - public static TimeSpan DefaultPollingInterval = TimeSpan.FromSeconds(30); + public static TimeSpan DefaultPollingInterval = TimeSpan.FromMinutes(5); + /// + /// Minimum value for . + /// + public static TimeSpan MinimumPollingInterval = TimeSpan.FromMinutes(5); /// /// Default value for . /// @@ -174,7 +178,11 @@ public class Configuration : IMobileConfiguration /// /// The default value for . /// - private static readonly TimeSpan DefaultBackgroundPollingInterval = TimeSpan.FromMinutes(3600); + private static readonly TimeSpan DefaultBackgroundPollingInterval = TimeSpan.FromMinutes(60); + /// + /// The minimum value for . + /// + public static readonly TimeSpan MinimumBackgroundPollingInterval = TimeSpan.FromMinutes(15); /// /// The default value for . /// @@ -367,10 +375,10 @@ public static Configuration WithEventSamplingInterval(this Configuration configu /// the same Configuration instance public static Configuration WithPollingInterval(this Configuration configuration, TimeSpan pollingInterval) { - if (pollingInterval.CompareTo(Configuration.DefaultPollingInterval) < 0) + if (pollingInterval.CompareTo(Configuration.MinimumPollingInterval) < 0) { - Log.Warn("PollingInterval cannot be less than the default of 30 seconds."); - pollingInterval = Configuration.DefaultPollingInterval; + Log.WarnFormat("PollingInterval cannot be less than the default of {0}."); + pollingInterval = Configuration.MinimumPollingInterval; } configuration.PollingInterval = pollingInterval; return configuration; @@ -653,6 +661,11 @@ public static Configuration WithEnableBackgroundUpdating(this Configuration conf /// the same Configuration instance public static Configuration WithBackgroundPollingInterval(this Configuration configuration, TimeSpan backgroundPollingInternal) { + if (backgroundPollingInternal.CompareTo(Configuration.MinimumBackgroundPollingInterval) < 0) + { + Log.WarnFormat("BackgroundPollingInterval cannot be less than the default of {0}.", Configuration.MinimumBackgroundPollingInterval); + backgroundPollingInternal = Configuration.MinimumBackgroundPollingInterval; + } configuration.BackgroundPollingInterval = backgroundPollingInternal; return configuration; } diff --git a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs index af1c0fc7..8f5b4f8d 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs +++ b/tests/LaunchDarkly.Xamarin.Tests/ConfigurationTest.cs @@ -13,12 +13,12 @@ public void CanOverrideConfiguration() var config = Configuration.Default("AnyOtherSdkKey") .WithBaseUri("https://app.AnyOtherEndpoint.com") .WithEventQueueCapacity(99) - .WithPollingInterval(TimeSpan.FromMinutes(1)); + .WithPollingInterval(TimeSpan.FromMinutes(45)); Assert.Equal(new Uri("https://app.AnyOtherEndpoint.com"), config.BaseUri); Assert.Equal("AnyOtherSdkKey", config.MobileKey); Assert.Equal(99, config.EventQueueCapacity); - Assert.Equal(TimeSpan.FromMinutes(1), config.PollingInterval); + Assert.Equal(TimeSpan.FromMinutes(45), config.PollingInterval); } [Fact] @@ -49,11 +49,19 @@ public void MobileKeyCannotBeEmpty() } [Fact] - public void CannotOverrideTooSmallPollingInterval() + public void CannotSetTooSmallPollingInterval() { - var config = Configuration.Default("AnyOtherSdkKey").WithPollingInterval(TimeSpan.FromSeconds(29)); + var config = Configuration.Default("AnyOtherSdkKey").WithPollingInterval(TimeSpan.FromSeconds(299)); - Assert.Equal(TimeSpan.FromSeconds(30), config.PollingInterval); + Assert.Equal(TimeSpan.FromSeconds(300), config.PollingInterval); + } + + [Fact] + public void CannotSetTooSmallBackgroundPollingInterval() + { + var config = Configuration.Default("SdkKey").WithBackgroundPollingInterval(TimeSpan.FromSeconds(899)); + + Assert.Equal(TimeSpan.FromSeconds(900), config.BackgroundPollingInterval); } [Fact] From 08e08f77009d5aef7e53904e8d2b5e7bcd83d4b2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 14 Aug 2018 11:40:16 -0700 Subject: [PATCH 45/47] bump LD.Common to 1.0.5 to get fix for reconnection delay --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- .../LaunchDarkly.Xamarin.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 51d35c2d..68087523 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -18,7 +18,7 @@ - + diff --git a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj index 0b13954b..400a99fc 100644 --- a/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj +++ b/tests/LaunchDarkly.Xamarin.Tests/LaunchDarkly.Xamarin.Tests.csproj @@ -9,7 +9,7 @@ - + From 0b66a68f02d42205366cd1193b18179211e439a2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 14 Aug 2018 11:50:08 -0700 Subject: [PATCH 46/47] 1.0.0-beta14 --- src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj index 68087523..d4e9e124 100644 --- a/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj +++ b/src/LaunchDarkly.Xamarin/LaunchDarkly.Xamarin.csproj @@ -1,7 +1,7 @@ - 1.0.0-beta13 + 1.0.0-beta14 Library LaunchDarkly.Xamarin LaunchDarkly.Xamarin From 63ed5ba2c09f0d0b6b7b35c1fc6a8bc08ac4f1b4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 10 Sep 2018 11:59:30 -0700 Subject: [PATCH 47/47] fix sample code --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d2065665..609f27aa 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,15 @@ Quick setup Install-Package LaunchDarkly.Xamarin -1. Import the LaunchDarkly package: +1. Import the LaunchDarkly packages: + using LaunchDarkly.Client; using LaunchDarkly.Xamarin; 2. Initialize the LDClient with your Mobile key and user: User user = User.WithKey(username); - LdClient ldClient = LdClient.Init("YOUR_MOBILE_KEY", username); + LdClient ldClient = LdClient.Init("YOUR_MOBILE_KEY", user); Your first feature flag -----------------------