1+ using System . Reflection ;
2+ using System . Text . RegularExpressions ;
3+ using Microsoft . Extraction . Tests ;
4+ using Semmle . Extraction ;
5+ using Semmle . Extraction . PowerShell . Standalone ;
6+ using Xunit . Abstractions ;
7+ using Xunit . Sdk ;
8+ using Semmle . Extraction . PowerShell ;
9+
10+ namespace Microsoft . Extractor . Tests ;
11+
12+ internal static class PathHolder
13+ {
14+ internal static string powershellSource = Path . Join ( ".." , ".." , ".." , ".." , ".." , "samples" , "code" ) ;
15+ internal static string expectedTraps = Path . Join ( ".." , ".." , ".." , ".." , ".." , "samples" , "traps" ) ;
16+ internal static string schemaPath = Path . Join ( ".." , ".." , ".." , ".." , ".." , "config" , "semmlecode.powershell.dbscheme" ) ;
17+ internal static string generatedTraps = Path . Join ( "." , Path . GetFullPath ( powershellSource ) . Replace ( ":" , "_" ) ) ;
18+ }
19+ public class TrapTestFixture : IDisposable
20+ {
21+ public TrapTestFixture ( )
22+ {
23+ // Setup here
24+ }
25+
26+ public void Dispose ( )
27+ {
28+ // Delete the generated traps
29+ Directory . Delete ( PathHolder . generatedTraps , true ) ;
30+ }
31+ }
32+
33+ public class Traps : IClassFixture < TrapTestFixture >
34+ {
35+ private readonly ITestOutputHelper _output ;
36+ public Traps ( ITestOutputHelper output )
37+ {
38+ _output = output ;
39+ }
40+
41+ private static Regex schemaDeclStart = new ( "([a-zA-Z_]+)\\ (" ) ;
42+ private static Regex schemaEnd = new ( "^\\ )" ) ;
43+ private static Regex commentEnd = new ( "\\ */" ) ;
44+
45+ /// <summary>
46+ /// Naiively parse the schema and try to determine how many parameters each table expects
47+ /// </summary>
48+ /// <param name="schemaContents"></param>
49+ /// <returns>Dictionary mapping table name to number of parameters</returns>
50+ private static Dictionary < string , int > ParseSchema ( string [ ] schemaContents )
51+ {
52+ bool isParsingTable = false ;
53+ int expectedNumEntries = 0 ;
54+ string targetName = string . Empty ;
55+ Dictionary < string , int > output = new ( ) ;
56+ for ( int index = 0 ; index < schemaContents . Length ; index ++ )
57+ {
58+ if ( ! isParsingTable )
59+ {
60+ if ( schemaDeclStart . IsMatch ( schemaContents [ index ] ) )
61+ {
62+ targetName = schemaDeclStart . Matches ( schemaContents [ index ] ) [ 0 ] . Groups [ 1 ] . Captures [ 0 ] . Value ;
63+ isParsingTable = true ;
64+ expectedNumEntries = 0 ;
65+ }
66+ }
67+ else
68+ {
69+ if ( commentEnd . IsMatch ( schemaContents [ index ] ) )
70+ {
71+ isParsingTable = false ;
72+ expectedNumEntries = 0 ;
73+ }
74+ if ( schemaEnd . IsMatch ( schemaContents [ index ] ) )
75+ {
76+ output . Add ( targetName , expectedNumEntries ) ;
77+ isParsingTable = false ;
78+ expectedNumEntries ++ ;
79+ }
80+ else
81+ {
82+ expectedNumEntries ++ ;
83+ }
84+ }
85+ }
86+
87+ return output ;
88+ }
89+
90+ /// <summary>
91+ /// Check that the Schema entries match the implemented methods in Tuples.cs
92+ /// </summary>
93+ [ Fact ]
94+ public void Schema_Matches_Tuples ( )
95+ {
96+ string [ ] schemaContents = File . ReadLines ( PathHolder . schemaPath ) . ToArray ( ) ;
97+ Dictionary < string , int > expected = ParseSchema ( schemaContents ) ;
98+ // Get all the nonpublic static methods from the Tuples classes
99+ var methods = typeof ( Semmle . Extraction . PowerShell . Tuples )
100+ . GetMethods ( BindingFlags . Static | BindingFlags . NonPublic )
101+ . Union ( typeof ( Semmle . Extraction . Tuples ) . GetMethods ( BindingFlags . Static | BindingFlags . NonPublic ) )
102+ // Select a tuple of the method, its parameters
103+ . Select ( method => ( method , method . GetParameters ( ) ,
104+ // the expected number of parameters - one fewer than actual if the first is a TextWriter, and the name of the method
105+ method . GetParameters ( ) [ 0 ] . ParameterType . Name . Equals ( "TextWriter" ) ? method . GetParameters ( ) . Length - 1 : method . GetParameters ( ) . Length , method . Name ) ) ;
106+ List < string > errors = new ( ) ;
107+ List < string > warnings = new ( ) ;
108+ // If a tuple method exists and doesn't have a matching schema entry that is an error, as the produce traps won't be match
109+ foreach ( var method in methods )
110+ {
111+ if ( expected . Any ( entry => method . Name == entry . Key && ( method . Item3 ) == entry . Value ) )
112+ {
113+ continue ;
114+ }
115+ errors . Add ( $ "Tuple { method . Name } does not match any schema entry, expected { method . Item3 } parameters.") ;
116+ }
117+ // If the schema has a superfluous entity that is a warning, as the extractor simply cannot product those things
118+ foreach ( var entry in expected )
119+ {
120+ if ( methods . Any ( method => method . Name == entry . Key && ( method . Item3 ) == entry . Value ) )
121+ {
122+ continue ;
123+ }
124+ warnings . Add ( $ "Schema entry { entry . Key } does not match any implemented Tuple, expected { entry . Value } parameters.") ;
125+ }
126+
127+ foreach ( var warning in warnings )
128+ {
129+ _output . WriteLine ( $ "Warning: { warning } ") ;
130+ }
131+ foreach ( var error in errors )
132+ {
133+ _output . WriteLine ( $ "Error: { error } ") ;
134+ }
135+ Assert . Empty ( errors ) ;
136+ }
137+
138+ [ Fact ]
139+ public void Verify_Sample_Traps ( )
140+ {
141+ string [ ] expectedTrapsFiles = Directory . GetFiles ( PathHolder . expectedTraps ) ;
142+ int numFailures = 0 ;
143+ foreach ( string expected in expectedTrapsFiles )
144+ {
145+ if ( File . ReadAllText ( expected ) . Contains ( "extractor_messages" ) )
146+ {
147+ numFailures ++ ;
148+ _output . WriteLine ( $ "Expected sample trap { expected } has extractor error messages.") ;
149+ }
150+ }
151+
152+ if ( numFailures > 0 )
153+ {
154+ _output . WriteLine ( $ "{ numFailures } errors were detected.") ;
155+ }
156+ Assert . Equal ( 0 , numFailures ) ;
157+ }
158+
159+
160+ [ Fact ]
161+ public void Compare_Generated_Traps ( )
162+ {
163+ string [ ] args = new string [ ] { PathHolder . powershellSource } ;
164+ int exitcode = Program . Main ( args ) ;
165+ Assert . Equal ( 0 , exitcode ) ;
166+ string [ ] generatedTrapsFiles = Directory . GetFiles ( PathHolder . generatedTraps ) ;
167+ string [ ] expectedTrapsFiles = Directory . GetFiles ( PathHolder . expectedTraps ) ;
168+
169+ Assert . NotEmpty ( generatedTrapsFiles ) ;
170+ int numFailures = 0 ;
171+ var generatedFileNames = generatedTrapsFiles . Select ( x => ( Path . GetFileName ( x ) , x ) ) . ToList ( ) ;
172+ var expectedFileNames = expectedTrapsFiles . Select ( x => ( Path . GetFileName ( x ) , x ) ) . ToList ( ) ;
173+ foreach ( var expectedTrapFile in expectedFileNames )
174+ {
175+ if ( generatedFileNames . Any ( x => x . Item1 == expectedTrapFile . Item1 ) ) continue ;
176+ numFailures ++ ;
177+ _output . WriteLine ( $ "{ expectedTrapFile } has no matching filename in generated.") ;
178+ }
179+ foreach ( var generated in generatedFileNames )
180+ {
181+ var expected = expectedFileNames . FirstOrDefault ( filePath => filePath . Item1 . Equals ( generated . Item1 ) ) ;
182+ if ( expected . Item1 is null || expected . x is null )
183+ {
184+ numFailures ++ ;
185+ _output . WriteLine ( $ "{ generated . Item1 } has no matching filename in expected.") ;
186+ }
187+ else
188+ {
189+ if ( File . ReadAllText ( generated . x ) . Contains ( "extractor_messages" ) )
190+ {
191+ _output . WriteLine ( $ "Test generated trap { generated } has extractor error messages.") ;
192+ numFailures ++ ;
193+ continue ;
194+ }
195+ string generatedFileSanitized = TrapSanitizer . SanitizeTrap ( File . ReadAllLines ( generated . x ) ) ;
196+ string expectedFileSanitized = TrapSanitizer . SanitizeTrap ( File . ReadAllLines ( expected . x ) ) ;
197+ if ( ! generatedFileSanitized . Equals ( expectedFileSanitized ) )
198+ {
199+ numFailures ++ ;
200+ _output . WriteLine ( $ "{ generated } does not match { expected } ") ;
201+ }
202+ }
203+ }
204+
205+ if ( numFailures > 0 )
206+ {
207+ _output . WriteLine ( $ "{ numFailures } errors were detected.") ;
208+ }
209+ Assert . Equal ( expectedTrapsFiles . Length , generatedTrapsFiles . Length ) ;
210+ Assert . Equal ( 0 , numFailures ) ;
211+ }
212+ }
0 commit comments