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+ }
0 commit comments