Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<LangVersion>13.0</LangVersion>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand Down
17 changes: 10 additions & 7 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestFeature"
},
"msbuild-sdks": {
"MSTest.Sdk": "3.6.3"
}
"sdk": {
"version": "10.0.100",
"rollForward": "latestMajor"
},
"msbuild-sdks": {
"MSTest.Sdk": "4.0.2"
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Import Project="..\Directory.Build.props" />

<PropertyGroup>
<TargetFrameworks>net9.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
</PropertyGroup>

<PropertyGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/FeatureSwitches.MSTest/FeatureSwitches.MSTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="MSTest.TestFramework" Version="3.6.3" />
<PackageReference Include="MSTest.TestFramework" Version="4.0.2" />
</ItemGroup>
</Project>
46 changes: 33 additions & 13 deletions src/FeatureSwitches.MSTest/FeatureTestMethodAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using FeatureSwitches.Definitions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[assembly: CLSCompliant(true)]

Expand All @@ -9,22 +9,32 @@ namespace FeatureSwitches.MSTest;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class FeatureTestMethodAttribute : TestMethodAttribute
{
private static readonly char[] ArgumentSeparator = [','];
private static readonly ConcurrentDictionary<string, IReadOnlyList<FeatureDefinition>> ExecutingTestFeatures = new();

/// <summary>
/// Initializes a new instance of the <see cref="FeatureTestMethodAttribute"/> class.
/// </summary>
/// <param name="onOff">A comma separated string of features to vary between on/off.</param>
/// <param name="on">A comma separated string of features that are always on.</param>
/// <param name="off">A comma separated string of features that are always off.</param>
/// <param name="displayName">The display name.</param>
public FeatureTestMethodAttribute(string? onOff, string? on = null, string? off = null, string? displayName = null)
: base(displayName)
public FeatureTestMethodAttribute(
string? onOff,
string? on = null,
string? off = null,
string? displayName = null,
#pragma warning disable CA1019 // Define accessors for attribute arguments
#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
#pragma warning disable SA1611 // Element parameters should be documented
[CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = -1)
#pragma warning restore SA1611 // Element parameters should be documented
#pragma warning restore CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
#pragma warning restore CA1019 // Define accessors for attribute arguments
: base(callerFilePath, callerLineNumber)
{
this.OnOff = onOff;
this.On = on;
this.Off = off;
this.DisplayName = displayName;
}

/// <summary>
Expand All @@ -47,29 +57,29 @@ public static IReadOnlyList<FeatureDefinition> GetFeatures(TestContext context)
{
ArgumentNullException.ThrowIfNull(context);

if (ExecutingTestFeatures.TryGetValue(context.FullyQualifiedTestClassName + '/' + context.TestName, out var features))
if (FeatureTestMethodAttributeHelpers.ExecutingTestFeatures.TryGetValue(context.FullyQualifiedTestClassName + '/' + context.TestName, out var features))
{
return features;
}

return [];
}

public override TestResult[] Execute(ITestMethod testMethod)
public override async Task<TestResult[]> ExecuteAsync(ITestMethod testMethod)
{
ArgumentNullException.ThrowIfNull(testMethod);

static string[] Convert(string? arg)
{
return arg?.Split(ArgumentSeparator, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray() ?? [];
return arg?.Split(FeatureTestMethodAttributeHelpers.ArgumentSeparator, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToArray() ?? [];
}

var on = Convert(this.On);
var off = Convert(this.Off);
var onOff = Convert(this.OnOff);

var results = new List<TestResult>();
var featuresTestValues = testMethod.GetAttributes<FeatureTestValueAttribute>(false);
var featuresTestValues = testMethod.GetAttributes<FeatureTestValueAttribute>();
var allFeatures = on.Concat(off).Concat(onOff);

var onCombinations = Enumerable.Range(0, 1 << onOff.Length)
Expand Down Expand Up @@ -124,9 +134,9 @@ static string[] Convert(string? arg)

var fullMethodName = testMethod.TestClassName + '/' + testMethod.TestMethodName;
var sortedFeatures = featureDefinitions.OrderBy(x => x.Name).ToList();
ExecutingTestFeatures.TryAdd(fullMethodName, sortedFeatures);
FeatureTestMethodAttributeHelpers.ExecutingTestFeatures.TryAdd(fullMethodName, sortedFeatures);

var result = testMethod.Invoke(null);
var result = await testMethod.InvokeAsync(null).ConfigureAwait(false);

static string GetOnOffValue(object? value)
{
Expand All @@ -142,10 +152,20 @@ static string GetOnOffValue(object? value)

results.Add(result);

ExecutingTestFeatures.TryRemove(fullMethodName, out _);
FeatureTestMethodAttributeHelpers.ExecutingTestFeatures.TryRemove(fullMethodName, out _);
}
}

return [.. results];
}

// This internal static class is a workaround for the diagnostic warning:
// " MSTEST0057: TestMethodAttribute derived class 'FeatureTestMethodAttribute' should add CallerFilePath and CallerLineNumber parameters to its constructor (https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0057 "
// The diagnostic does not recognize that when adding static fields a static constructor is generated by the compiler,
// and that static constructor can not have the CallerFilePath and CallerLineNumber parameters.
internal static class FeatureTestMethodAttributeHelpers
{
internal static readonly char[] ArgumentSeparator = [','];
internal static readonly ConcurrentDictionary<string, IReadOnlyList<FeatureDefinition>> ExecutingTestFeatures = new();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
<PropertyGroup>
<Description>ServiceCollection extensions for FeatureSwitches</Description>
<PackageId>FeatureSwitches.ServiceCollection</PackageId>
<Version>9.0.0</Version>
<Version>10.0.0</Version>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions src/FeatureSwitches/IFeatureService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace FeatureSwitches;
namespace FeatureSwitches;

public interface IFeatureService
{
Expand Down Expand Up @@ -57,7 +57,7 @@ public interface IFeatureService
/// <param name="feature">The featureswitch name.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The current switch value, or null if the feature doesn't exist.</returns>
public Task<byte[]?> GetBytes(string feature, CancellationToken cancellationToken = default);
Task<byte[]?> GetBytes(string feature, CancellationToken cancellationToken = default);

/// <summary>
/// Gets the current value of the featureswitch within the specified evaluation context as a byte array.
Expand All @@ -68,5 +68,5 @@ public interface IFeatureService
/// <param name="cancellationToken">A cancellation token.</param>
/// <typeparam name="TEvaluationContext">The evaluation context type.</typeparam>
/// <returns>The current switch value, or null if the feature doesn't exist.</returns>
public Task<byte[]?> GetBytes<TEvaluationContext>(string feature, TEvaluationContext evaluationContext, CancellationToken cancellationToken = default);
Task<byte[]?> GetBytes<TEvaluationContext>(string feature, TEvaluationContext evaluationContext, CancellationToken cancellationToken = default);
}
2 changes: 1 addition & 1 deletion test/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Import Project="..\Directory.Build.props" />

<PropertyGroup>
<TargetFrameworks>net9.0</TargetFrameworks>
<TargetFrameworks>net10.0</TargetFrameworks>
<IsPackable>false</IsPackable>
</PropertyGroup>

Expand Down
2 changes: 1 addition & 1 deletion test/FeatureSwitches.Test/FeatureSwitches.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="MSTest.Sdk">
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public async Task Invalid_request()
var featureDatabase = this.sp.GetRequiredService<InMemoryFeatureDefinitionProvider>();
featureDatabase.SetFeature("Egg");
var featureService = this.sp.GetRequiredService<FeatureService>();
await Assert.ThrowsExceptionAsync<JsonException>(() => featureService.GetValue<TestVariation>("Egg"));
await Assert.ThrowsAsync<JsonException>(() => featureService.GetValue<TestVariation>("Egg"));
Assert.IsTrue(await featureService.IsOn("Egg"));
}

Expand All @@ -124,7 +124,7 @@ public async Task IsOn_with_non_boolean_feature()
featureDatabase.SetFeature("Switch", isOn: true, offValue: "Off", onValue: "On");

var featureService = this.sp.GetRequiredService<FeatureService>();
await Assert.ThrowsExceptionAsync<JsonException>(() => featureService.IsOn("Switch"));
await Assert.ThrowsAsync<JsonException>(() => featureService.IsOn("Switch"));
}

[TestMethod]
Expand Down Expand Up @@ -474,7 +474,7 @@ public async Task All_features()

var featureService = this.sp.GetRequiredService<FeatureService>();
var features = await featureService.GetFeatures();
Assert.AreEqual(2, features.Length);
Assert.HasCount(2, features);
Assert.AreEqual("FeatureA", features[0]);
Assert.AreEqual("FeatureB", features[1]);
}
Expand Down
Loading