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