1
+ using System . Diagnostics ;
2
+ using System . Text ;
3
+ using DiffPlex . DiffBuilder ;
4
+ using DiffPlex . DiffBuilder . Model ;
5
+ using Scip ;
6
+ using Index = Scip . Index ;
7
+
8
+ namespace ScipDotnet . Tests ;
9
+
10
+ [ TestFixture ]
11
+ public class SnapshotTests
12
+ {
13
+ [ Test , TestCaseSource ( nameof ( ListSnapshotInputDirectories ) ) ]
14
+ public void Snapshot ( string inputDirectory )
15
+ {
16
+ var indexFile = IndexDirectory ( inputDirectory ) ;
17
+ var indexBytes = File . ReadAllBytes ( indexFile ) ;
18
+ var index = Index . Parser . ParseFrom ( indexBytes ) ;
19
+ var snapshots = index . Documents . ToDictionary ( document => document . RelativePath ,
20
+ document => FormatDocument ( index , document ) ) ;
21
+ var outputDirectory =
22
+ Path . GetFullPath ( Path . Join ( inputDirectory , "../../" , "output" , Path . GetFileName ( inputDirectory ) ) ) ;
23
+ var isUpdateSnapshots = Environment . GetEnvironmentVariable ( "SCIP_UPDATE_SNAPSHOTS" ) != null ;
24
+ if ( isUpdateSnapshots )
25
+ {
26
+ if ( Directory . Exists ( outputDirectory ) )
27
+ {
28
+ Directory . Delete ( outputDirectory , true ) ;
29
+ }
30
+
31
+ foreach ( var ( relativePath , snapshot ) in snapshots )
32
+ {
33
+ var documentPath = Path . Join ( outputDirectory , relativePath ) ;
34
+ var directory = Path . GetDirectoryName ( documentPath ) ;
35
+ if ( directory == null ) continue ;
36
+ Directory . CreateDirectory ( directory ) ;
37
+ File . WriteAllText ( documentPath , snapshot ) ;
38
+ }
39
+ }
40
+ else
41
+ {
42
+ var files = new List < string > ( ) ;
43
+ RecursivelyListFiles ( outputDirectory , files ) ;
44
+ Assert . Multiple ( ( ) =>
45
+ {
46
+ foreach ( var file in files )
47
+ {
48
+ var relativePath = Path . GetRelativePath ( outputDirectory , file ) ;
49
+ var obtained = snapshots . GetValueOrDefault ( relativePath , "" ) ;
50
+ var expected = File . ReadAllText ( file ) ;
51
+ var diff = DiffStrings ( obtained , expected ) ;
52
+ if ( diff . Length > 0 )
53
+ {
54
+ Assert . Fail ( "(+ expected, - obtained). To update the expected output to match the obtained behavior, run: " +
55
+ "SCIP_UPDATE_SNAPSHOTS=true dotnet test\n \n " + diff , file ) ;
56
+ }
57
+ }
58
+
59
+ var filesSet = new HashSet < string > ( files ) ;
60
+ foreach ( var ( relativePath , _) in snapshots )
61
+ {
62
+ var outputPath = Path . Join ( outputDirectory , relativePath ) ;
63
+ if ( filesSet . Contains ( outputPath ) )
64
+ {
65
+ continue ;
66
+ }
67
+
68
+ Assert . Fail (
69
+ $ "relative path '{ relativePath } ' missing an output file. To fix this problem, run the following command: SCIP_UPDATE_SNAPSHOTS=true dotnet test") ;
70
+ }
71
+ } ) ;
72
+ }
73
+ }
74
+
75
+ private static string DiffStrings ( string obtained , string expected )
76
+ {
77
+ var diff = InlineDiffBuilder . Diff ( expected , obtained ) ;
78
+ var sb = new StringBuilder ( ) ;
79
+ for ( var i = 0 ; i < diff . Lines . Count ; i ++ )
80
+ {
81
+ var line = diff . Lines [ i ] ;
82
+ if ( line . Type == ChangeType . Unchanged )
83
+ {
84
+ continue ;
85
+ }
86
+
87
+ if ( i > 0 && diff . Lines [ i - 1 ] . Type == ChangeType . Unchanged )
88
+ {
89
+ sb . Append ( " " ) . AppendLine ( diff . Lines [ i - 1 ] . Text ) ;
90
+ }
91
+
92
+ switch ( line . Type )
93
+ {
94
+ case ChangeType . Inserted :
95
+ sb . Append ( "+ " ) ;
96
+ break ;
97
+ case ChangeType . Deleted :
98
+ sb . Append ( "- " ) ;
99
+ break ;
100
+ }
101
+
102
+ sb . AppendLine ( line . Text ) ;
103
+ }
104
+
105
+ return sb . ToString ( ) ;
106
+ }
107
+
108
+ private static string [ ] ListSnapshotInputDirectories ( )
109
+ {
110
+ var inputs = Path . Join ( RootDirectory ( ) , "snapshots" , "input" ) ;
111
+ return Directory . GetDirectories ( inputs ) ;
112
+ }
113
+
114
+ private static void RecursivelyListFiles ( string path , List < string > result )
115
+ {
116
+ if ( ! Directory . Exists ( path ) ) return ;
117
+ result . AddRange ( Directory . GetFiles ( path ) ) ;
118
+ foreach ( var directory in Directory . GetDirectories ( path ) )
119
+ {
120
+ RecursivelyListFiles ( directory , result ) ;
121
+ }
122
+ }
123
+
124
+ private static string IndexDirectory ( string directory )
125
+ {
126
+ var include = Environment . GetEnvironmentVariable ( "SCIP_INCLUDE" ) ;
127
+ var includeOption = include != null ? $ " --include { include } " : "" ;
128
+ var arguments = $ "run --project ScipDotnet -- index --working-directory { directory } { includeOption } ";
129
+ var process = new Process ( )
130
+ {
131
+ StartInfo = new ProcessStartInfo ( )
132
+ {
133
+ FileName = "dotnet" ,
134
+ Arguments = arguments ,
135
+ UseShellExecute = false ,
136
+ RedirectStandardOutput = true ,
137
+ WorkingDirectory = RootDirectory ( ) ,
138
+ RedirectStandardError = true
139
+ }
140
+ } ;
141
+ process . Start ( ) ;
142
+ process . WaitForExit ( ) ;
143
+ if ( process . ExitCode != 0 )
144
+ {
145
+ var stdout = process . StandardOutput . ReadToEnd ( ) ;
146
+ var stderr = process . StandardError . ReadToEnd ( ) ;
147
+ Assert . Fail (
148
+ $ "non-zero exit code { process . ExitCode } indexing { directory } \n dotnet { arguments } \n { stdout } { stderr } ") ;
149
+ }
150
+
151
+ return Path . Join ( directory , "index.scip" ) ;
152
+ }
153
+
154
+ private static string RootDirectory ( )
155
+ {
156
+ var process = new Process ( )
157
+ {
158
+ StartInfo = new ProcessStartInfo ( )
159
+ {
160
+ // The working directory of `dotnet test` is not the root directory of the project
161
+ // so we infer it by invoking `git rev-parse --show-toplevel`. It would be cleaner
162
+ // to get the root directory from MSBuild but I wasn't able to figure out how to do it
163
+ // after searching for ~20 minutes. This works for now and unblocks writing tests.
164
+ FileName = "git" ,
165
+ Arguments = "rev-parse --show-toplevel" ,
166
+ UseShellExecute = false ,
167
+ RedirectStandardOutput = true
168
+ }
169
+ } ;
170
+ process . Start ( ) ;
171
+ return process . StandardOutput . ReadToEnd ( ) . Trim ( ) ;
172
+ }
173
+
174
+
175
+ private Dictionary < String , SymbolInformation > SymbolTable ( Document document )
176
+ {
177
+ var result = new Dictionary < String , SymbolInformation > ( ) ;
178
+ foreach ( var info in document . Symbols )
179
+ {
180
+ // Intentionally crash the test if we emit conflicting SymbolInformation
181
+ if ( ! result . TryAdd ( info . Symbol , info ) )
182
+ {
183
+ Assert . Fail ( "duplicate SymbolInformation '{0}'" , info . Symbol ) ;
184
+ }
185
+ }
186
+
187
+ return result ;
188
+ }
189
+
190
+ private string FormatDocument ( Index index , Document document )
191
+ {
192
+ var sb = new StringBuilder ( ) ;
193
+ var inputPath = Path . Join (
194
+ index . Metadata . ProjectRoot . Replace ( "file://" , String . Empty ) ,
195
+ document . RelativePath ) ;
196
+ var occurrences = document . Occurrences . ToList ( ) ;
197
+ occurrences . Sort ( CompareOccurrences ) ;
198
+ var symtab = SymbolTable ( document ) ;
199
+ var occurrenceIndex = 0 ;
200
+ var lines = File . ReadAllLines ( inputPath ) ;
201
+ for ( var lineNumber = 0 ; lineNumber < lines . Length ; lineNumber ++ )
202
+ {
203
+ var line = lines [ lineNumber ] . Replace ( "\t " , " " ) ;
204
+ sb . Append ( " " ) . AppendLine ( line ) ;
205
+ while ( occurrenceIndex < occurrences . Count )
206
+ {
207
+ var occurrence = occurrences [ occurrenceIndex ] ;
208
+ var range = Range . FromOccurrence ( occurrence ) ;
209
+ if ( range . Start . Line != range . End . Line )
210
+ continue ; // TODO: support in the future
211
+ if ( range . Start . Line != lineNumber )
212
+ break ;
213
+ var isDefinition = ( occurrence . SymbolRoles & ( int ) SymbolRole . Definition ) != 0 ;
214
+ var role = isDefinition ? "definition" : "reference" ;
215
+ var length = range . End . Character - range . Start . Character ;
216
+ var indent = new String ( ' ' , range . Start . Character ) ;
217
+ sb . Append ( "//" )
218
+ . Append ( indent )
219
+ . Append ( new String ( '^' , length ) )
220
+ . Append ( ' ' )
221
+ . Append ( role )
222
+ . Append ( ' ' )
223
+ . AppendLine ( occurrence . Symbol ) ;
224
+
225
+ if ( isDefinition )
226
+ {
227
+ var info = symtab . GetValueOrDefault ( occurrence . Symbol , new SymbolInformation ( ) ) ;
228
+ var prefix = "//" + indent + new String ( ' ' , length + 1 ) ;
229
+ foreach ( var documentation in info . Documentation )
230
+ {
231
+ sb . Append ( prefix ) . Append ( "documentation " ) . AppendLine ( documentation . Replace ( "\n " , "\\ n" ) ) ;
232
+ }
233
+
234
+ foreach ( var relationship in info . Relationships )
235
+ {
236
+ sb . Append ( prefix ) . Append ( "relationship " ) ;
237
+ if ( relationship . IsDefinition ) sb . Append ( "definition " ) ;
238
+ if ( relationship . IsImplementation ) sb . Append ( "implementation " ) ;
239
+ if ( relationship . IsReference ) sb . Append ( "reference " ) ;
240
+ if ( relationship . IsTypeDefinition ) sb . Append ( "type_definition " ) ;
241
+ sb . AppendLine ( relationship . Symbol ) ;
242
+ }
243
+ }
244
+
245
+ occurrenceIndex ++ ;
246
+ }
247
+ }
248
+
249
+ return sb . ToString ( ) ;
250
+ }
251
+
252
+ private static int CompareOccurrences ( Occurrence a , Occurrence b )
253
+ {
254
+ return Range . FromOccurrence ( a ) . CompareTo ( Range . FromOccurrence ( b ) ) ;
255
+ }
256
+ }
0 commit comments