1+ namespace NServiceBus . Persistence . CosmosDB . Analyzers . Test ;
2+
3+ using System ;
4+ using System . Collections . Generic ;
5+ using System . Collections . Immutable ;
6+ using System . Linq ;
7+ using System . Reflection ;
8+ using System . Text ;
9+ using System . Text . RegularExpressions ;
10+ using System . Threading ;
11+ using System . Threading . Tasks ;
12+ using Microsoft . Azure . Cosmos ;
13+ using Microsoft . CodeAnalysis ;
14+ using Microsoft . CodeAnalysis . CSharp ;
15+ using Microsoft . CodeAnalysis . Diagnostics ;
16+ using Microsoft . CodeAnalysis . Text ;
17+ using NServiceBus . TransactionalSession ;
18+ using NUnit . Framework ;
19+
20+ public class AnalyzerTestFixture < TAnalyzer > where TAnalyzer : DiagnosticAnalyzer , new ( )
21+ {
22+ protected virtual LanguageVersion AnalyzerLanguageVersion => LanguageVersion . CSharp12 ;
23+
24+ protected Task Assert ( string markupCode , CancellationToken cancellationToken = default ) =>
25+ Assert ( [ ] , markupCode , [ ] , cancellationToken ) ;
26+
27+ protected Task Assert ( string expectedDiagnosticId , string markupCode , CancellationToken cancellationToken = default ) =>
28+ Assert ( [ expectedDiagnosticId ] , markupCode , [ ] , cancellationToken ) ;
29+
30+ protected async Task Assert ( string [ ] expectedDiagnosticIds , string markupCode , string [ ] ignoreDiagnosticIds , CancellationToken cancellationToken = default )
31+ {
32+ var ( code , markupSpans ) = Parse ( markupCode ) ;
33+
34+ var project = CreateProject ( code ) ;
35+ await WriteCode ( project , cancellationToken ) ;
36+
37+ var compilerDiagnostics = ( await Task . WhenAll ( project . Documents
38+ . Select ( doc => doc . GetCompilerDiagnostics ( cancellationToken ) ) ) )
39+ . SelectMany ( diagnostics => diagnostics ) ;
40+
41+ WriteCompilerDiagnostics ( compilerDiagnostics ) ;
42+
43+ var compilation = await project . GetCompilationAsync ( cancellationToken ) ;
44+ compilation . Compile ( ) ;
45+
46+ var analyzerDiagnostics = ( await compilation . GetAnalyzerDiagnostics ( new TAnalyzer ( ) , cancellationToken ) )
47+ . Where ( d => ! ignoreDiagnosticIds . Contains ( d . Id ) )
48+ . ToList ( ) ;
49+ WriteAnalyzerDiagnostics ( analyzerDiagnostics ) ;
50+
51+ var expectedSpansAndIds = expectedDiagnosticIds
52+ . SelectMany ( id => markupSpans . Select ( span => ( span . file , span . span , id ) ) )
53+ . OrderBy ( item => item . span )
54+ . ThenBy ( item => item . id )
55+ . ToList ( ) ;
56+
57+ var actualSpansAndIds = analyzerDiagnostics
58+ . Select ( diagnostic => ( diagnostic . Location . SourceTree . FilePath , diagnostic . Location . SourceSpan , diagnostic . Id ) )
59+ . ToList ( ) ;
60+
61+ NUnit . Framework . Assert . That ( actualSpansAndIds , Is . EqualTo ( expectedSpansAndIds ) . AsCollection ) ;
62+ }
63+
64+ protected static async Task WriteCode ( Project project , CancellationToken cancellationToken = default )
65+ {
66+ if ( ! VerboseLogging )
67+ {
68+ return ;
69+ }
70+
71+ foreach ( var document in project . Documents )
72+ {
73+ Console . WriteLine ( document . Name ) ;
74+ var code = await document . GetCode ( cancellationToken ) ;
75+ foreach ( var ( line , index ) in code . Replace ( "\r \n " , "\n " ) . Split ( '\n ' )
76+ . Select ( ( line , index ) => ( line , index ) ) )
77+ {
78+ Console . WriteLine ( $ " { index + 1 , 3 } : { line } ") ;
79+ }
80+ }
81+
82+ }
83+
84+ static readonly ImmutableDictionary < string , ReportDiagnostic > DiagnosticOptions = new Dictionary < string , ReportDiagnostic >
85+ {
86+ { "CS1701" , ReportDiagnostic . Hidden }
87+ }
88+ . ToImmutableDictionary ( ) ;
89+
90+ protected Project CreateProject ( string [ ] code )
91+ {
92+ var workspace = new AdhocWorkspace ( ) ;
93+ var project = workspace . AddProject ( "TestProject" , LanguageNames . CSharp )
94+ . WithCompilationOptions ( new CSharpCompilationOptions ( OutputKind . DynamicallyLinkedLibrary )
95+ . WithSpecificDiagnosticOptions ( DiagnosticOptions ) )
96+ . WithParseOptions ( new CSharpParseOptions ( AnalyzerLanguageVersion ) )
97+ . AddMetadataReferences ( ProjectReferences ) ;
98+
99+ for ( int i = 0 ; i < code . Length ; i ++ )
100+ {
101+ project = project . AddDocument ( $ "TestDocument{ i } ", code [ i ] ) . Project ;
102+ }
103+
104+ return project ;
105+ }
106+
107+ static AnalyzerTestFixture ( ) =>
108+ ProjectReferences = ImmutableList . Create (
109+ MetadataReference . CreateFromFile ( typeof ( object ) . GetTypeInfo ( ) . Assembly . Location ) ,
110+ MetadataReference . CreateFromFile ( typeof ( Enumerable ) . GetTypeInfo ( ) . Assembly . Location ) ,
111+ MetadataReference . CreateFromFile ( typeof ( System . Linq . Expressions . Expression ) . GetTypeInfo ( ) . Assembly
112+ . Location ) ,
113+ MetadataReference . CreateFromFile ( Assembly . Load ( "System.Runtime" ) . Location ) ,
114+ MetadataReference . CreateFromFile ( typeof ( CosmosClient ) . GetTypeInfo ( ) . Assembly . Location ) ,
115+ MetadataReference . CreateFromFile ( typeof ( EndpointConfiguration ) . GetTypeInfo ( ) . Assembly . Location ) ,
116+ MetadataReference . CreateFromFile ( typeof ( CosmosPersistenceConfig ) . GetTypeInfo ( ) . Assembly . Location ) ,
117+ MetadataReference . CreateFromFile ( typeof ( CosmosOpenSessionOptions ) . GetTypeInfo ( ) . Assembly . Location ) ) ;
118+
119+ static readonly ImmutableList < PortableExecutableReference > ProjectReferences ;
120+
121+ static readonly Regex DocumentSplittingRegex = new Regex ( "^-{5,}.*" , RegexOptions . Compiled | RegexOptions . Multiline ) ;
122+
123+ protected static void WriteCompilerDiagnostics ( IEnumerable < Diagnostic > diagnostics )
124+ {
125+ if ( ! VerboseLogging )
126+ {
127+ return ;
128+ }
129+
130+ Console . WriteLine ( "Compiler diagnostics:" ) ;
131+
132+ foreach ( var diagnostic in diagnostics )
133+ {
134+ Console . WriteLine ( $ " { diagnostic } ") ;
135+ }
136+ }
137+
138+ protected static void WriteAnalyzerDiagnostics ( IEnumerable < Diagnostic > diagnostics )
139+ {
140+ if ( ! VerboseLogging )
141+ {
142+ return ;
143+ }
144+
145+ Console . WriteLine ( "Analyzer diagnostics:" ) ;
146+
147+ foreach ( var diagnostic in diagnostics )
148+ {
149+ Console . WriteLine ( $ " { diagnostic } ") ;
150+ }
151+ }
152+
153+ protected static string [ ] SplitMarkupCodeIntoFiles ( string markupCode ) =>
154+ DocumentSplittingRegex . Split ( markupCode )
155+ . Where ( docCode => ! string . IsNullOrWhiteSpace ( docCode ) )
156+ . ToArray ( ) ;
157+
158+ static ( string [ ] code , List < ( string file , TextSpan span ) > ) Parse ( string markupCode )
159+ {
160+ if ( markupCode == null )
161+ {
162+ return ( [ ] , [ ] ) ;
163+ }
164+
165+ var documents = SplitMarkupCodeIntoFiles ( markupCode ) ;
166+
167+ var markupSpans = new List < ( string , TextSpan ) > ( ) ;
168+
169+ for ( var i = 0 ; i < documents . Length ; i ++ )
170+ {
171+ var code = new StringBuilder ( ) ;
172+ var name = $ "TestDocument{ i } ";
173+
174+ var remainingCode = documents [ i ] ;
175+ var remainingCodeStart = 0 ;
176+
177+ while ( remainingCode . Length > 0 )
178+ {
179+ var beforeAndAfterOpening = remainingCode . Split ( [ "[|" ] , 2 , StringSplitOptions . None ) ;
180+
181+ if ( beforeAndAfterOpening . Length == 1 )
182+ {
183+ _ = code . Append ( beforeAndAfterOpening [ 0 ] ) ;
184+ break ;
185+ }
186+
187+ var midAndAfterClosing = beforeAndAfterOpening [ 1 ] . Split ( [ "|]" ] , 2 , StringSplitOptions . None ) ;
188+
189+ if ( midAndAfterClosing . Length == 1 )
190+ {
191+ throw new Exception ( "The markup code does not contain a closing '|]'" ) ;
192+ }
193+
194+ var markupSpan = new TextSpan ( remainingCodeStart + beforeAndAfterOpening [ 0 ] . Length , midAndAfterClosing [ 0 ] . Length ) ;
195+
196+ _ = code . Append ( beforeAndAfterOpening [ 0 ] ) . Append ( midAndAfterClosing [ 0 ] ) ;
197+ markupSpans . Add ( ( name , markupSpan ) ) ;
198+
199+ remainingCode = midAndAfterClosing [ 1 ] ;
200+ remainingCodeStart += beforeAndAfterOpening [ 0 ] . Length + markupSpan . Length ;
201+ }
202+
203+ documents [ i ] = code . ToString ( ) ;
204+ }
205+
206+ return ( documents , markupSpans ) ;
207+ }
208+
209+ protected static readonly bool VerboseLogging = Environment . GetEnvironmentVariable ( "CI" ) != "true"
210+ || Environment . GetEnvironmentVariable ( "VERBOSE_TEST_LOGGING" ) ? . ToLower ( ) == "true" ;
211+ }
0 commit comments