Skip to content

Commit

Permalink
Additional minor improvements (#51)
Browse files Browse the repository at this point in the history
* Improves error reporting when setting type and default value type mismatch

* Improves ConfigDeserializer performance by using ETag for change detection instead of calculating a hash

* Improves SHA1 hashing implementation

* Eliminates ModuleInitalizer trick used in tests because MSTest provides that feature out of the box
  • Loading branch information
adams85 authored Nov 10, 2022
1 parent 5eaf3a2 commit 1f721c4
Show file tree
Hide file tree
Showing 18 changed files with 334 additions and 119 deletions.
3 changes: 0 additions & 3 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ The files containing such source code are the following:
* src/ConfigCatClient/Versioning/IntExtensions.cs, which
is originally available at https://github.com/maxhauser/semver/blob/v2.2.0/Semver/Utility/IntExtensions.cs and
is licensed under MIT license (see https://github.com/maxhauser/semver/blob/v2.2.0/License.txt).
* src/ConfigCat.Client.Tests/ModuleInitializerAttribute.cs, which
is originally available at https://github.com/dotnet/runtime/blob/v6.0.9/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/ModuleInitializerAttribute.cs and
is licensed under MIT license (see https://github.com/dotnet/runtime/blob/main/LICENSE.TXT).
See the file headers for details.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Net;
using System.Runtime.CompilerServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ConfigCat.Client.Tests
{
internal class ModuleInitializer
[TestClass]
public class AssemblyInitializer
{
[ModuleInitializer]
internal static void Setup()
[AssemblyInitialize]
public static void AssemblyInitialize(TestContext context)
{
#if NET45
// TLS 1.2 was not enabled before .NET 4.6 by default (see https://stackoverflow.com/a/58195987/8656352),
Expand Down
72 changes: 72 additions & 0 deletions src/ConfigCat.Client.Tests/BasicConfigEvaluatorTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
using ConfigCat.Client.Evaluation;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace ConfigCat.Client.Tests
{
Expand Down Expand Up @@ -48,5 +53,72 @@ public void GetValue_WithUser_ShouldReturnEvaluatedValue()

Assert.AreEqual(3.1415, actual);
}

private delegate object EvaluateDelegate(IRolloutEvaluator evaluator, IDictionary<string, Setting> settings, string key, object defaultValue, User user,
ProjectConfig remoteConfig, ILogger logger, out EvaluationDetails<object> evaluationDetails);

private static readonly MethodInfo evaluateMethodDefinition = new EvaluateDelegate(RolloutEvaluatorExtensions.Evaluate).Method.GetGenericMethodDefinition();

[DataRow("stringDefaultCat", "", "Cat", typeof(string))]
[DataRow("stringDefaultCat", "", "Cat", typeof(object))]
[DataRow("boolDefaultTrue", false, true, typeof(bool))]
[DataRow("boolDefaultTrue", false, true, typeof(bool?))]
[DataRow("boolDefaultTrue", false, true, typeof(object))]
[DataRow("integerDefaultOne", 0, 1, typeof(int))]
[DataRow("integerDefaultOne", 0, 1, typeof(int?))]
[DataRow("integerDefaultOne", 0L, 1L, typeof(long))]
[DataRow("integerDefaultOne", 0L, 1L, typeof(long?))]
[DataRow("integerDefaultOne", 0, 1, typeof(object))]
[DataRow("doubleDefaultPi", 0.0, 3.1415, typeof(double))]
[DataRow("doubleDefaultPi", 0.0, 3.1415, typeof(double?))]
[DataRow("doubleDefaultPi", 0.0, 3.1415, typeof(object))]
[DataTestMethod]
public void GetValue_WithCompatibleDefaultValue_ShouldSucceed(string key, object defaultValue, object expectedValue, Type settingClrType)
{
var args = new object[]
{
this.configEvaluator,
this.config,
key,
defaultValue,
null,
null,
this.logger,
null
};

var actualValue = evaluateMethodDefinition.MakeGenericMethod(settingClrType).Invoke(null, args);
var evaluationDetails = (EvaluationDetails)args.Last();

Assert.AreEqual(expectedValue, actualValue);
Assert.AreEqual(expectedValue, evaluationDetails.Value);
}

[DataRow("stringDefaultCat", 0.0, typeof(double?))]
[DataRow("boolDefaultTrue", "false", typeof(string))]
[DataRow("integerDefaultOne", "0", typeof(string))]
[DataRow("doubleDefaultPi", 0, typeof(int))]
[DataTestMethod]
public void GetValue_WithIncompatibleDefaultValueType_ShouldThrowWithImprovedErrorMessage(string key, object defaultValue, Type settingClrType)
{
var args = new object[]
{
this.configEvaluator,
this.config,
key,
defaultValue,
null,
null,
this.logger,
null
};

var ex = Assert.ThrowsException<InvalidOperationException>(() =>
{
try { evaluateMethodDefinition.MakeGenericMethod(settingClrType).Invoke(null, args); }
catch (TargetInvocationException ex) { throw ex.InnerException; }
});
StringAssert.Contains(ex.Message, $"Setting's type was {this.config[key].SettingType} but the default value's type was {settingClrType}.");
}
}
}
10 changes: 5 additions & 5 deletions src/ConfigCat.Client.Tests/ConfigCatClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ public void GetAllKeys_DeserializerThrowException_ShouldReturnsWithEmptyArray()
configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty);
var o = new SettingsWithPreferences();
configDeserializerMock
.Setup(m => m.TryDeserialize(It.IsAny<string>(), out o))
.Setup(m => m.TryDeserialize(It.IsAny<string>(), It.IsAny<string>(), out o))
.Throws<Exception>();

IConfigCatClient instance = new ConfigCatClient(
Expand Down Expand Up @@ -734,7 +734,7 @@ public async Task GetAllKeysAsync_DeserializeFailed_ShouldReturnsWithEmptyArray(
configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty);
var o = new SettingsWithPreferences();
configDeserializerMock
.Setup(m => m.TryDeserialize(It.IsAny<string>(), out o))
.Setup(m => m.TryDeserialize(It.IsAny<string>(), It.IsAny<string>(), out o))
.Returns(false);

IConfigCatClient instance = new ConfigCatClient(
Expand Down Expand Up @@ -762,7 +762,7 @@ public void GetAllKeys_DeserializeFailed_ShouldReturnsWithEmptyArray()
configServiceMock.Setup(m => m.GetConfig()).Returns(ProjectConfig.Empty);
var o = new SettingsWithPreferences();
configDeserializerMock
.Setup(m => m.TryDeserialize(It.IsAny<string>(), out o))
.Setup(m => m.TryDeserialize(It.IsAny<string>(), It.IsAny<string>(), out o))
.Returns(false);

IConfigCatClient instance = new ConfigCatClient(
Expand Down Expand Up @@ -849,7 +849,7 @@ public void GetVariationId_DeserializeFailed_ShouldReturnsWithEmptyArray()
configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty);
var o = new SettingsWithPreferences();
configDeserializerMock
.Setup(m => m.TryDeserialize(It.IsAny<string>(), out o))
.Setup(m => m.TryDeserialize(It.IsAny<string>(), It.IsAny<string>(), out o))
.Returns(false);

IConfigCatClient instance = new ConfigCatClient(
Expand Down Expand Up @@ -877,7 +877,7 @@ public async Task GetVariationIdAsync_DeserializeFailed_ShouldReturnsWithEmptyAr
configServiceMock.Setup(m => m.GetConfigAsync()).ReturnsAsync(ProjectConfig.Empty);
var o = new SettingsWithPreferences();
configDeserializerMock
.Setup(m => m.TryDeserialize(It.IsAny<string>(), out o))
.Setup(m => m.TryDeserialize(It.IsAny<string>(), It.IsAny<string>(), out o))
.Returns(false);

IConfigCatClient instance = new ConfigCatClient(
Expand Down
4 changes: 2 additions & 2 deletions src/ConfigCat.Client.Tests/ConfigEvaluatorTestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ protected virtual void AssertValue(string keyName, string expected, User user)

protected string GetSampleJson()
{
using Stream stream = File.OpenRead("data" + Path.DirectorySeparatorChar + this.SampleJsonFileName);
using Stream stream = File.OpenRead(Path.Combine("data", this.SampleJsonFileName));
using StreamReader reader = new(stream);
return reader.ReadToEnd();
}

public async Task MatrixTest(Action<string, string, User> assertation)
{
using Stream stream = File.OpenRead("data" + Path.DirectorySeparatorChar + this.MatrixResultFileName);
using Stream stream = File.OpenRead(Path.Combine("data", this.MatrixResultFileName));
using StreamReader reader = new(stream);
var header = await reader.ReadLineAsync();

Expand Down
2 changes: 1 addition & 1 deletion src/ConfigCat.Client.Tests/DeserializerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public void Ensure_Global_Settings_Doesnt_Interfere()
};

var deserializer = new ConfigDeserializer();
deserializer.TryDeserialize("{\"p\": {\"u\": \"http://example.com\", \"r\": 0}}", out var configs);
deserializer.TryDeserialize("{\"p\": {\"u\": \"http://example.com\", \"r\": 0}}", httpETag: null, out var configs);
}
}
}
37 changes: 0 additions & 37 deletions src/ConfigCat.Client.Tests/ModuleInitializerAttribute.cs

This file was deleted.

8 changes: 6 additions & 2 deletions src/ConfigCatClient/ConfigCatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ public T GetValue<T>(string key, T defaultValue, User user = null)
user ??= this.defaultUser;
try
{
typeof(T).EnsureSupportedSettingClrType();
settings = this.GetSettings();
value = this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log, out evaluationDetails);
}
Expand All @@ -280,6 +281,7 @@ public async Task<T> GetValueAsync<T>(string key, T defaultValue, User user = nu
user ??= this.defaultUser;
try
{
typeof(T).EnsureSupportedSettingClrType();
settings = await this.GetSettingsAsync().ConfigureAwait(false);
value = this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log, out evaluationDetails);
}
Expand All @@ -302,6 +304,7 @@ public EvaluationDetails<T> GetValueDetails<T>(string key, T defaultValue, User
user ??= this.defaultUser;
try
{
typeof(T).EnsureSupportedSettingClrType();
settings = this.GetSettings();
this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log, out evaluationDetails);
}
Expand All @@ -323,6 +326,7 @@ public async Task<EvaluationDetails<T>> GetValueDetailsAsync<T>(string key, T de
user ??= this.defaultUser;
try
{
typeof(T).EnsureSupportedSettingClrType();
settings = await this.GetSettingsAsync().ConfigureAwait(false);
this.configEvaluator.Evaluate(settings.Value, key, defaultValue, user, settings.RemoteConfig, this.log, out evaluationDetails);
}
Expand Down Expand Up @@ -684,7 +688,7 @@ private SettingsWithRemoteConfig GetSettings()
SettingsWithRemoteConfig GetRemoteConfig()
{
var config = this.configService.GetConfig();
if (!this.configDeserializer.TryDeserialize(config.JsonString, out var deserialized))
if (!this.configDeserializer.TryDeserialize(config.JsonString, config.HttpETag, out var deserialized))
return new SettingsWithRemoteConfig(new Dictionary<string, Setting>(), config);

return new SettingsWithRemoteConfig(deserialized.Settings, config);
Expand Down Expand Up @@ -717,7 +721,7 @@ private async Task<SettingsWithRemoteConfig> GetSettingsAsync()
async Task<SettingsWithRemoteConfig> GetRemoteConfigAsync()
{
var config = await this.configService.GetConfigAsync().ConfigureAwait(false);
if (!this.configDeserializer.TryDeserialize(config.JsonString, out var deserialized))
if (!this.configDeserializer.TryDeserialize(config.JsonString, config.HttpETag, out var deserialized))
return new SettingsWithRemoteConfig(new Dictionary<string, Setting>(), config);

return new SettingsWithRemoteConfig(deserialized.Settings, config);
Expand Down
1 change: 0 additions & 1 deletion src/ConfigCatClient/ConfigCatClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ Works with .NET, .NET Core, .NET Standard</Description>
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Data.HashFunction.MurmurHash" Version="2.0.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<!--Do not remove this reference, it was added due to a SNYK security report-->
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
Expand Down
26 changes: 13 additions & 13 deletions src/ConfigCatClient/ConfigDeserializer.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
using ConfigCat.Client.Evaluation;
using System;
using System.Data.HashFunction;
using System.Data.HashFunction.MurmurHash;

namespace ConfigCat.Client
{
internal class ConfigDeserializer : IConfigDeserializer
{
private readonly IHashFunction hasher = MurmurHash3Factory.Instance.Create(new MurmurHash3Config { HashSizeInBits = 128 });
private SettingsWithPreferences lastSerializedSettings;
private byte[] hashToCompare;
private SettingsWithPreferences lastDeserializedSettings;
private string lastConfig;
private string lastHttpETag;

public bool TryDeserialize(string config, out SettingsWithPreferences settings)
public bool TryDeserialize(string config, string httpETag, out SettingsWithPreferences settings)
{
if (config == null)
{
settings = null;
return false;
}

var hash = this.hasher.ComputeHash(config).Hash;
if (CompareByteArrays(this.hashToCompare, hash))
var configContentHasChanged = this.lastHttpETag is not null && httpETag is not null
? this.lastHttpETag != httpETag
: this.lastConfig != config;

if (!configContentHasChanged)
{
settings = this.lastSerializedSettings;
settings = this.lastDeserializedSettings;
return true;
}

this.lastSerializedSettings = settings = config.Deserialize<SettingsWithPreferences>();
this.hashToCompare = hash;
this.lastDeserializedSettings = settings = config.Deserialize<SettingsWithPreferences>();
this.lastConfig = config;
this.lastHttpETag = httpETag;
return true;
}

private static bool CompareByteArrays(ReadOnlySpan<byte> b1, ReadOnlySpan<byte> b2) => b1.SequenceEqual(b2);
}
}
Loading

0 comments on commit 1f721c4

Please sign in to comment.