Skip to content

Commit f9a1d60

Browse files
committed
featureflags
1 parent 7562aef commit f9a1d60

File tree

7 files changed

+192
-0
lines changed

7 files changed

+192
-0
lines changed

Drift.sln

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Networking.PeerStreaming.Te
8787
EndProject
8888
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agent.PeerProtocol", "src\Agent.PeerProtocol\Agent.PeerProtocol.csproj", "{7C72C2AE-2888-47A0-AAA4-61CC66B9F941}"
8989
EndProject
90+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureFlagsDELETE", "src\FeatureFlagsDELETE\FeatureFlagsDELETE.csproj", "{31593F51-0B46-4A2B-AA60-88A10D3195F3}"
91+
EndProject
92+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureFlagsDELETE.Tests", "src\FeatureFlagsDELETE.Tests\FeatureFlagsDELETE.Tests.csproj", "{24EA16E4-A73B-42F5-B315-FB9AE1B3F9AF}"
93+
EndProject
9094
Global
9195
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9296
Debug|Any CPU = Debug|Any CPU
@@ -215,6 +219,14 @@ Global
215219
{7C72C2AE-2888-47A0-AAA4-61CC66B9F941}.Debug|Any CPU.Build.0 = Debug|Any CPU
216220
{7C72C2AE-2888-47A0-AAA4-61CC66B9F941}.Release|Any CPU.ActiveCfg = Release|Any CPU
217221
{7C72C2AE-2888-47A0-AAA4-61CC66B9F941}.Release|Any CPU.Build.0 = Release|Any CPU
222+
{31593F51-0B46-4A2B-AA60-88A10D3195F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
223+
{31593F51-0B46-4A2B-AA60-88A10D3195F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
224+
{31593F51-0B46-4A2B-AA60-88A10D3195F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
225+
{31593F51-0B46-4A2B-AA60-88A10D3195F3}.Release|Any CPU.Build.0 = Release|Any CPU
226+
{24EA16E4-A73B-42F5-B315-FB9AE1B3F9AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
227+
{24EA16E4-A73B-42F5-B315-FB9AE1B3F9AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
228+
{24EA16E4-A73B-42F5-B315-FB9AE1B3F9AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
229+
{24EA16E4-A73B-42F5-B315-FB9AE1B3F9AF}.Release|Any CPU.Build.0 = Release|Any CPU
218230
EndGlobalSection
219231
GlobalSection(NestedProjects) = preSolution
220232
{8523E9E0-F412-41B7-B361-ADE639FFAF24} = {C0698EF0-61C8-403E-8E93-1F1D34C5B910}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Reflection;
2+
using NUnit.Framework.Interfaces;
3+
using NUnit.Framework.Internal;
4+
using NUnit.Framework.Internal.Builders;
5+
using NUnit.Framework.Internal.Commands;
6+
7+
namespace Drift.FeatureFlagsDELETE.Tests;
8+
9+
[AttributeUsage( AttributeTargets.Method, AllowMultiple = false )]
10+
public class FeatureFlagMatrixAttribute : NUnitAttribute, ITestBuilder, IWrapTestMethod {
11+
private readonly NUnitTestCaseBuilder _builder = new();
12+
13+
// -----------------------
14+
// ITestBuilder: generates all test cases
15+
// -----------------------
16+
public IEnumerable<TestMethod> BuildFrom( IMethodInfo method, Test? suite ) {
17+
var sources = method.GetCustomAttributes<TestCaseSourceAttribute>( true );
18+
19+
if ( sources.Any() ) {
20+
// Cross-product of each TestCaseSource with feature flags
21+
foreach ( var sourceAttr in sources ) {
22+
var sourceData = GetTestCaseSourceData( method, sourceAttr );
23+
24+
foreach ( var caseData in sourceData ) {
25+
foreach ( var flags in FeatureFlagService.GetAllCombinations( includeEmpty: false ) ) {
26+
var originalArgs = ExtractArgumentsFromCaseData( caseData );
27+
var parms = new TestCaseParameters( originalArgs );
28+
parms.Properties.Set( "FeatureFlags", flags );
29+
30+
// Add flag info to test name
31+
parms.TestName = caseData is TestCaseData tcd && tcd.TestName != null
32+
? $"{tcd.TestName} [{string.Join( ",", flags )}]"
33+
: $"{method.Name} [{string.Join( ",", flags )}]";
34+
35+
yield return _builder.BuildTestMethod( method, suite, parms );
36+
}
37+
}
38+
}
39+
}
40+
else {
41+
// No TestCaseSource: one test per feature flag combination
42+
foreach ( var flags in FeatureFlagService.GetAllCombinations( includeEmpty: false ) ) {
43+
var parms = new TestCaseParameters( new object[] { flags } );
44+
parms.Properties.Set( "FeatureFlags", flags );
45+
parms.TestName = $"{method.Name} [{string.Join( ",", flags )}]";
46+
47+
yield return _builder.BuildTestMethod( method, suite, parms );
48+
}
49+
}
50+
}
51+
52+
// -----------------------
53+
// IWrapTestMethod: ensures flags are accessed
54+
// -----------------------
55+
public TestCommand Wrap( TestCommand command ) {
56+
return new FeatureFlagCheckCommand( command );
57+
}
58+
59+
private class FeatureFlagCheckCommand : DelegatingTestCommand {
60+
public FeatureFlagCheckCommand( TestCommand inner ) : base( inner ) {
61+
}
62+
63+
public override TestResult Execute( TestExecutionContext context ) {
64+
bool accessedFlags = false;
65+
66+
// Provide a way for the test to access feature flags
67+
context.CurrentTest.Properties.Set( "GetFeatureFlags", new Func<HashSet<FeatureFlag>>( () => {
68+
accessedFlags = true;
69+
return (HashSet<FeatureFlag>) context.CurrentTest.Properties.Get( "FeatureFlags" )!;
70+
} ) );
71+
72+
var result = innerCommand.Execute( context );
73+
74+
// Fail the test if the flags were never accessed
75+
if ( !accessedFlags ) {
76+
result.SetResult( ResultState.Failure, "FeatureFlags were never accessed." );
77+
}
78+
79+
return result;
80+
}
81+
}
82+
83+
// -----------------------
84+
// Helpers
85+
// -----------------------
86+
private static object[] ExtractArgumentsFromCaseData( object caseData ) {
87+
return caseData switch {
88+
TestCaseData tcd => tcd.Arguments,
89+
object[] arr => arr,
90+
_ => new object[] { caseData }
91+
};
92+
}
93+
94+
private static IEnumerable<object> GetTestCaseSourceData( IMethodInfo method, TestCaseSourceAttribute attr ) {
95+
MemberInfo? sourceMember = attr.SourceType != null
96+
? attr.SourceType.GetMember( attr.SourceName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic )
97+
.FirstOrDefault()
98+
: method.TypeInfo.Type
99+
.GetMember( attr.SourceName, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic )
100+
.FirstOrDefault();
101+
102+
if ( sourceMember == null )
103+
throw new InvalidOperationException(
104+
$"Cannot find member {attr.SourceName} on type {( attr.SourceType ?? method.TypeInfo.Type ).FullName}" );
105+
106+
return sourceMember switch {
107+
MethodInfo mi => mi.Invoke( null, null ) as IEnumerable<object> ?? Enumerable.Empty<object>(),
108+
PropertyInfo pi => pi.GetValue( null ) as IEnumerable<object> ?? Enumerable.Empty<object>(),
109+
FieldInfo fi => fi.GetValue( null ) as IEnumerable<object> ?? Enumerable.Empty<object>(),
110+
_ => throw new InvalidOperationException( $"Unsupported member type for {attr.SourceName}" )
111+
};
112+
}
113+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<ItemGroup>
4+
<Using Include="NUnit.Framework" />
5+
</ItemGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Microsoft.NET.Test.Sdk" />
9+
<PackageReference Include="NUnit" />
10+
<PackageReference Include="NUnit.Analyzers" />
11+
<PackageReference Include="NUnit3TestAdapter" />
12+
<PackageReference Include="Verify.NUnit" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<ProjectReference Include="..\FeatureFlagsDELETE\FeatureFlagsDELETE.csproj" />
17+
</ItemGroup>
18+
19+
</Project>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Drift.FeatureFlagsDELETE.Tests;
2+
3+
public static class TestContextExtensions {
4+
public static HashSet<FeatureFlag> GetFeatureFlags( this TestContext testContext ) {
5+
var getFlags = (Func<HashSet<FeatureFlag>>) TestContext.CurrentContext.Test.Properties.Get( "GetFeatureFlags" )!;
6+
return getFlags();
7+
}
8+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
namespace Drift.FeatureFlagsDELETE;
2+
3+
public class FeatureFlagService {
4+
private readonly HashSet<FeatureFlag> _enabledFlags;
5+
6+
public FeatureFlagService( IEnumerable<FeatureFlag> enabledFlags ) {
7+
_enabledFlags = new HashSet<FeatureFlag>( enabledFlags );
8+
}
9+
10+
public bool IsEnabled( FeatureFlag flag ) => _enabledFlags.Contains( flag );
11+
12+
public static IEnumerable<HashSet<FeatureFlag>> GetAllCombinations( bool includeEmpty = true ) {
13+
var flags = Enum.GetValues<FeatureFlag>();
14+
int totalCombinations = 1 << flags.Length; // 2^n combinations
15+
16+
for ( int i = 0; i < totalCombinations; i++ ) {
17+
// Skip the empty set if includeEmpty is false
18+
if ( !includeEmpty && i == 0 ) {
19+
continue;
20+
}
21+
22+
var combination = new HashSet<FeatureFlag>();
23+
for ( int j = 0; j < flags.Length; j++ ) {
24+
if ( ( i & ( 1 << j ) ) != 0 ) {
25+
combination.Add( flags[j] );
26+
}
27+
}
28+
29+
yield return combination;
30+
}
31+
}
32+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Drift.FeatureFlagsDELETE;
2+
3+
public enum FeatureFlag {
4+
Agent,
5+
RandomFeature
6+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
</Project>

0 commit comments

Comments
 (0)