@@ -42,7 +42,7 @@ public void AddResolvedFunctions_ResolvesAndAggregates_WithoutSymbolDownload() {
4242 using var profiler = new FunctionProfiler ( options , new NoSymbolLocator ( ) ) ;
4343
4444 profiler . AddImages ( SyntheticSampleBuilder . CreateImages ( ( module , baseAddr , size ) ) ) ;
45- profiler . AddResolvedFunctions ( module , new List < FunctionDebugInfo > {
45+ profiler . AddResolvedFunctions ( module , baseAddr , new List < FunctionDebugInfo > {
4646 new ( "Main" , 0x1000 , 0x800 ) ,
4747 new ( "Foo" , 0x2000 , 0x800 ) ,
4848 new ( "Bar" , 0x3000 , 0x800 )
@@ -77,14 +77,116 @@ public void AddResolvedFunctions_ResolvesAndAggregates_WithoutSymbolDownload() {
7777 }
7878
7979 [ TestMethod ]
80- public void AddResolvedFunctions_NullArguments_Throw ( ) {
80+ public void AddResolvedFunctions_InvalidArguments_Throw ( ) {
81+ const string module = "app.dll" ;
82+ const long baseAddr = 0x140000000 ;
83+ const int size = 0x100000 ;
8184 var options = new ProfilerOptions { SymbolPaths = new [ ] { "srv*https://symbols.invalid" } } ;
8285 using var profiler = new FunctionProfiler ( options , new NoSymbolLocator ( ) ) ;
86+ profiler . AddImages ( SyntheticSampleBuilder . CreateImages ( ( module , baseAddr , size ) ) ) ;
8387
84- Assert . ThrowsException < ArgumentException > ( ( ) =>
85- profiler . AddResolvedFunctions ( "" , new List < FunctionDebugInfo > ( ) ) ) ;
88+ // Null function list.
8689 Assert . ThrowsException < ArgumentNullException > ( ( ) =>
87- profiler . AddResolvedFunctions ( "m.dll" , null ! ) ) ;
90+ profiler . AddResolvedFunctions ( module , baseAddr , null ! ) ) ;
91+ // Empty module name.
92+ Assert . ThrowsException < ArgumentException > ( ( ) =>
93+ profiler . AddResolvedFunctions ( "" , baseAddr , new List < FunctionDebugInfo > ( ) ) ) ;
94+ // Base address was never registered via AddImages.
95+ Assert . ThrowsException < ArgumentException > ( ( ) =>
96+ profiler . AddResolvedFunctions ( module , 0xDEADBEEF , new List < FunctionDebugInfo > ( ) ) ) ;
97+ // Module name does not match the image registered at that base (wrong-module guard).
98+ Assert . ThrowsException < ArgumentException > ( ( ) =>
99+ profiler . AddResolvedFunctions ( "wrong.dll" , baseAddr , new List < FunctionDebugInfo > ( ) ) ) ;
100+ }
101+
102+ [ TestMethod ]
103+ public void AddResolvedFunctions_ModuleNameCaseMismatch_Throws ( ) {
104+ const string module = "App.DLL" ;
105+ const long baseAddr = 0x140000000 ;
106+ const int size = 0x100000 ;
107+ var options = new ProfilerOptions {
108+ SymbolPaths = new [ ] { "srv*https://symbols.invalid" } ,
109+ IncludeManagedCode = false ,
110+ IncludePerformanceCounters = false
111+ } ;
112+ using var profiler = new FunctionProfiler ( options , new NoSymbolLocator ( ) ) ;
113+ profiler . AddImages ( SyntheticSampleBuilder . CreateImages ( ( module , baseAddr , size ) ) ) ;
114+
115+ // Module identity is case-sensitive (Ordinal) throughout the library: a case-only difference denotes
116+ // a different binary (the WinUI vs UWP xaml pair), so the wrong-module guard rejects it.
117+ Assert . ThrowsException < ArgumentException > ( ( ) =>
118+ profiler . AddResolvedFunctions ( "app.dll" , baseAddr ,
119+ new List < FunctionDebugInfo > { new ( "Foo" , 0x1000 , 0x800 ) } ) ) ;
120+ }
121+
122+ [ TestMethod ]
123+ public void AddImages_DuplicateSameImage_IsIdempotent ( ) {
124+ const string module = "app.dll" ;
125+ const long baseAddr = 0x140000000 ;
126+ const int size = 0x100000 ;
127+ var options = new ProfilerOptions {
128+ SymbolPaths = new [ ] { "srv*https://symbols.invalid" } ,
129+ IncludeManagedCode = false ,
130+ IncludePerformanceCounters = false
131+ } ;
132+ using var profiler = new FunctionProfiler ( options , new NoSymbolLocator ( ) ) ;
133+
134+ // The same image reported twice (e.g. an ImageDCStart rundown followed by its ImageLoad) is
135+ // deduped, matching old Core — resolution still works normally afterward.
136+ profiler . AddImages ( SyntheticSampleBuilder . CreateImages ( ( module , baseAddr , size ) ) ) ;
137+ profiler . AddImages ( SyntheticSampleBuilder . CreateImages ( ( module , baseAddr , size ) ) ) ;
138+
139+ profiler . AddResolvedFunctions ( module , baseAddr ,
140+ new List < FunctionDebugInfo > { new ( "Foo" , 0x1000 , 0x800 ) } ) ;
141+ profiler . AddSamples ( new IProfileSample [ ] {
142+ new SyntheticSample ( baseAddr + 0x1000 + 0x10 , TimeSpan . FromMilliseconds ( 5 ) , 1 , 1 , module , baseAddr )
143+ } ) ;
144+
145+ var report = profiler . GetReport ( ) ;
146+ Assert . AreEqual ( TimeSpan . FromMilliseconds ( 5 ) ,
147+ report . Functions [ new ProfileFunctionId ( module , "Foo" ) ] . ExclusiveWeight ) ;
148+ }
149+
150+ [ TestMethod ]
151+ public void AddImages_SameBaseDifferentImage_ThrowsByDefault ( ) {
152+ const long baseAddr = 0x140000000 ;
153+ const int size = 0x100000 ;
154+ var options = new ProfilerOptions {
155+ SymbolPaths = new [ ] { "srv*https://symbols.invalid" } ,
156+ IncludeManagedCode = false ,
157+ IncludePerformanceCounters = false
158+ } ;
159+ using var profiler = new FunctionProfiler ( options , new NoSymbolLocator ( ) ) ;
160+
161+ // Two DIFFERENT binaries at the same base means the caller didn't de-dupe per sampling window;
162+ // silently keeping one would mis-attribute the other's samples, so this throws by default.
163+ profiler . AddImages ( SyntheticSampleBuilder . CreateImages ( ( "first.dll" , baseAddr , size ) ) ) ;
164+ Assert . ThrowsException < InvalidOperationException > ( ( ) =>
165+ profiler . AddImages ( SyntheticSampleBuilder . CreateImages ( ( "second.dll" , baseAddr , size ) ) ) ) ;
166+ }
167+
168+ [ TestMethod ]
169+ public void AddImages_SameBaseDifferentImage_LenientMode_LastWins ( ) {
170+ const long baseAddr = 0x140000000 ;
171+ const int size = 0x100000 ;
172+ var options = new ProfilerOptions {
173+ SymbolPaths = new [ ] { "srv*https://symbols.invalid" } ,
174+ IncludeManagedCode = false ,
175+ IncludePerformanceCounters = false ,
176+ ThrowOnImageBaseCollision = false
177+ } ;
178+ using var profiler = new FunctionProfiler ( options , new NoSymbolLocator ( ) ) ;
179+
180+ // Opt-in lenient mode: the base-keyed resolver can hold only one, so the latest registration wins
181+ // (warned, non-fatal). Attaching functions to the displaced image is then rejected by the guard.
182+ profiler . AddImages ( SyntheticSampleBuilder . CreateImages ( ( "first.dll" , baseAddr , size ) ) ) ;
183+ profiler . AddImages ( SyntheticSampleBuilder . CreateImages ( ( "second.dll" , baseAddr , size ) ) ) ;
184+
185+ profiler . AddResolvedFunctions ( "second.dll" , baseAddr ,
186+ new List < FunctionDebugInfo > { new ( "Bar" , 0x1000 , 0x800 ) } ) ;
187+ Assert . ThrowsException < ArgumentException > ( ( ) =>
188+ profiler . AddResolvedFunctions ( "first.dll" , baseAddr ,
189+ new List < FunctionDebugInfo > { new ( "Foo" , 0x1000 , 0x800 ) } ) ) ;
88190 }
89191
90192 [ TestMethod ]
@@ -101,7 +203,7 @@ public void AddSamples_WithInstancePath_FocusesOnMatchingStacks() {
101203
102204 using var profiler = new FunctionProfiler ( options , new NoSymbolLocator ( ) ) ;
103205 profiler . AddImages ( SyntheticSampleBuilder . CreateImages ( ( module , baseAddr , size ) ) ) ;
104- profiler . AddResolvedFunctions ( module , new List < FunctionDebugInfo > {
206+ profiler . AddResolvedFunctions ( module , baseAddr , new List < FunctionDebugInfo > {
105207 new ( "Main" , 0x1000 , 0x800 ) ,
106208 new ( "Foo" , 0x2000 , 0x800 ) ,
107209 new ( "Bar" , 0x3000 , 0x800 ) ,
0 commit comments