@@ -22,6 +22,17 @@ namespace Microsoft.ClientModel.TestFramework;
2222public class TestProxyProcess
2323{
2424 private static readonly string s_dotNetExe ;
25+ private readonly int ? _proxyPortHttp ;
26+ private readonly int ? _proxyPortHttps ;
27+ private readonly Process ? _testProxyProcess ;
28+ private readonly StringBuilder _errorBuffer = new ( ) ;
29+ private static readonly object _lock = new ( ) ;
30+ private static TestProxyProcess ? _shared ;
31+ private readonly StringBuilder _output = new ( ) ;
32+ private static readonly bool s_enableDebugProxyLogging ;
33+
34+ internal virtual TestProxyAdminClient AdminClient { get ; }
35+ internal virtual TestProxyClient ProxyClient { get ; }
2536
2637 /// <summary>
2738 /// The IP address used for the test proxy. Uses 127.0.0.1 instead of localhost to avoid SSL callback slowness.
@@ -39,26 +50,6 @@ public class TestProxyProcess
3950 /// </summary>
4051 public int ? ProxyPortHttps => _proxyPortHttps ;
4152
42- private readonly int ? _proxyPortHttp ;
43- private readonly int ? _proxyPortHttps ;
44- private readonly Process ? _testProxyProcess ;
45-
46- /// <summary>
47- /// Gets the test framework client for interacting with the test proxy.
48- /// </summary>
49- internal virtual TestProxyAdminClient AdminClient { get ; }
50-
51- /// <summary>
52- /// Gets the test proxy client for proxy-specific operations.
53- /// </summary>
54- internal virtual TestProxyClient ProxyClient { get ; }
55-
56- private readonly StringBuilder _errorBuffer = new ( ) ;
57- private static readonly object _lock = new ( ) ;
58- private static TestProxyProcess ? _shared ;
59- private readonly StringBuilder _output = new ( ) ;
60- private static readonly bool s_enableDebugProxyLogging ;
61-
6253 /// <summary>
6354 /// Initializes static members of the <see cref="TestProxyProcess"/> class.
6455 /// Locates the .NET executable and configures debug logging settings.
@@ -102,24 +93,35 @@ private TestProxyProcess(string? proxyPath, bool debugMode = false)
10293
10394 debugMode |= environmentDebugMode ;
10495
105- ProcessStartInfo testProxyProcessInfo = new ProcessStartInfo (
106- s_dotNetExe ,
107- $ " \" { proxyPath } \" start -u --storage-location= \" { TestEnvironment . RepositoryRoot } \" " )
96+ ProcessStartInfo testProxyProcessInfo ;
97+
98+ if ( proxyPath is not null )
10899 {
109- UseShellExecute = false ,
110- RedirectStandardOutput = true ,
111- RedirectStandardError = true ,
112- EnvironmentVariables =
113- {
114- [ "ASPNETCORE_URLS" ] = $ "http://{ IpAddress } :0;https://{ IpAddress } :0",
115- [ "Logging__LogLevel__Azure.Sdk.Tools.TestProxy" ] = s_enableDebugProxyLogging ? "Debug" : "Error" ,
116- [ "Logging__LogLevel__Default" ] = "Error" ,
117- [ "Logging__LogLevel__Microsoft.AspNetCore" ] = s_enableDebugProxyLogging ? "Information" : "Error" ,
118- [ "Logging__LogLevel__Microsoft.Hosting.Lifetime" ] = "Information" ,
119- [ "ASPNETCORE_Kestrel__Certificates__Default__Path" ] = TestEnvironment . DevCertPath ,
120- [ "ASPNETCORE_Kestrel__Certificates__Default__Password" ] = TestEnvironment . DevCertPassword
121- }
122- } ;
100+ testProxyProcessInfo = new ProcessStartInfo (
101+ s_dotNetExe ,
102+ $ "\" { proxyPath } \" start -u --storage-location=\" { TestEnvironment . RepositoryRoot } \" ") ;
103+ }
104+ else
105+ {
106+ TryRestoreLocalTools ( ) ;
107+
108+ testProxyProcessInfo = new ProcessStartInfo (
109+ s_dotNetExe ,
110+ $ "tool run test-proxy start -u --storage-location=\" { TestEnvironment . RepositoryRoot } \" ") ;
111+ }
112+
113+ testProxyProcessInfo . UseShellExecute = false ;
114+ testProxyProcessInfo . RedirectStandardOutput = true ;
115+ testProxyProcessInfo . RedirectStandardError = true ;
116+
117+ // Set environment variables
118+ testProxyProcessInfo . EnvironmentVariables [ "ASPNETCORE_URLS" ] = $ "http://{ IpAddress } :0;https://{ IpAddress } :0";
119+ testProxyProcessInfo . EnvironmentVariables [ "Logging__LogLevel__Azure.Sdk.Tools.TestProxy" ] = s_enableDebugProxyLogging ? "Debug" : "Error" ;
120+ testProxyProcessInfo . EnvironmentVariables [ "Logging__LogLevel__Default" ] = "Error" ;
121+ testProxyProcessInfo . EnvironmentVariables [ "Logging__LogLevel__Microsoft.AspNetCore" ] = s_enableDebugProxyLogging ? "Information" : "Error" ;
122+ testProxyProcessInfo . EnvironmentVariables [ "Logging__LogLevel__Microsoft.Hosting.Lifetime" ] = "Information" ;
123+ testProxyProcessInfo . EnvironmentVariables [ "ASPNETCORE_Kestrel__Certificates__Default__Path" ] = TestEnvironment . DevCertPath ;
124+ testProxyProcessInfo . EnvironmentVariables [ "ASPNETCORE_Kestrel__Certificates__Default__Password" ] = TestEnvironment . DevCertPassword ;
123125
124126 _testProxyProcess = Process . Start ( testProxyProcessInfo ) ;
125127
@@ -168,7 +170,13 @@ private TestProxyProcess(string? proxyPath, bool debugMode = false)
168170
169171 if ( _proxyPortHttp == null || _proxyPortHttps == null )
170172 {
171- CheckForErrors ( ) ;
173+ if ( _errorBuffer . Length > 0 )
174+ {
175+ var error = _errorBuffer . ToString ( ) ;
176+ _errorBuffer . Clear ( ) ;
177+ throw new InvalidOperationException ( $ "An error occurred in the test proxy: { error } ") ;
178+ }
179+
172180 // if no errors, fallback to this exception
173181 throw new InvalidOperationException ( "Failed to start the test proxy. One or both of the ports was not populated." + Environment . NewLine +
174182 $ "http: { _proxyPortHttp } " + Environment . NewLine +
@@ -192,6 +200,69 @@ private TestProxyProcess(string? proxyPath, bool debugMode = false)
192200 } ) ;
193201 }
194202
203+ private static bool TryParsePort ( string ? output , string scheme , out int ? port )
204+ {
205+ if ( output == null )
206+ {
207+ TestContext . Progress . WriteLine ( "output was null" ) ;
208+ port = null ;
209+ return false ;
210+ }
211+ string nowListeningOn = "Now listening on: " ;
212+ int nowListeningOnLength = nowListeningOn . Length ;
213+ var index = output . IndexOf ( $ "{ nowListeningOn } { scheme } :", StringComparison . CurrentCultureIgnoreCase ) ;
214+ if ( index > - 1 )
215+ {
216+ var start = index + nowListeningOnLength ;
217+ var uri = output . Substring ( start , output . Length - start ) . Trim ( ) ;
218+ port = new Uri ( uri ) . Port ;
219+ return true ;
220+ }
221+
222+ port = null ;
223+ return false ;
224+ }
225+
226+ private static void TryRestoreLocalTools ( )
227+ {
228+ try
229+ {
230+ var currentDir = Directory . GetCurrentDirectory ( ) ;
231+ while ( currentDir != null )
232+ {
233+ var toolsJsonPath = Path . Combine ( currentDir , ".config" , "dotnet-tools.json" ) ;
234+ if ( File . Exists ( toolsJsonPath ) )
235+ {
236+ // Found a tools manifest, try to restore
237+ var processInfo = new ProcessStartInfo
238+ {
239+ FileName = s_dotNetExe ,
240+ Arguments = "tool restore" ,
241+ WorkingDirectory = currentDir ,
242+ UseShellExecute = false ,
243+ RedirectStandardOutput = true ,
244+ RedirectStandardError = true ,
245+ CreateNoWindow = true
246+ } ;
247+
248+ using var process = Process . Start ( processInfo ) ;
249+ if ( process != null )
250+ {
251+ process . WaitForExit ( 30000 ) ;
252+ }
253+ break ;
254+ }
255+
256+ var parentDir = Directory . GetParent ( currentDir ) ;
257+ currentDir = parentDir ? . FullName ;
258+ }
259+ }
260+ catch
261+ {
262+ // If restore fails, silently continue - the dotnet test-proxy command will handle it
263+ }
264+ }
265+
195266 /// <summary>
196267 /// Starts the test proxy
197268 /// </summary>
@@ -209,12 +280,8 @@ public static TestProxyProcess Start(bool debugMode = false)
209280 var shared = _shared ;
210281 if ( shared == null )
211282 {
212- shared = new TestProxyProcess ( typeof ( TestProxyProcess )
213- . Assembly
214- . GetCustomAttributes < AssemblyMetadataAttribute > ( )
215- . Single ( a => a . Key == "TestProxyPath" )
216- . Value ,
217- debugMode ) ;
283+ var proxyPath = GetTestProxyPath ( ) ;
284+ shared = new TestProxyProcess ( proxyPath , debugMode ) ;
218285
219286 AppDomain . CurrentDomain . DomainUnload += ( _ , _ ) =>
220287 {
@@ -228,34 +295,16 @@ public static TestProxyProcess Start(bool debugMode = false)
228295 }
229296 }
230297
231- /// <summary>
232- /// Attempts to parse a port number from test proxy output for the specified scheme.
233- /// </summary>
234- /// <param name="output">The output line from the test proxy.</param>
235- /// <param name="scheme">The URI scheme (http or https) to parse.</param>
236- /// <param name="port">When this method returns, contains the parsed port number if successful; otherwise, null.</param>
237- /// <returns>true if the port was successfully parsed; otherwise, false.</returns>
238- private static bool TryParsePort ( string ? output , string scheme , out int ? port )
298+ private static string ? GetTestProxyPath ( )
239299 {
240- if ( output == null )
241- {
242- TestContext . Progress . WriteLine ( "output was null" ) ;
243- port = null ;
244- return false ;
245- }
246- string nowListeningOn = "Now listening on: " ;
247- int nowListeningOnLength = nowListeningOn . Length ;
248- var index = output . IndexOf ( $ "{ nowListeningOn } { scheme } :", StringComparison . CurrentCultureIgnoreCase ) ;
249- if ( index > - 1 )
300+ // Look for environment variable override
301+ var envPath = Environment . GetEnvironmentVariable ( "TEST_PROXY_EXE_PATH" ) ;
302+ if ( ! string . IsNullOrEmpty ( envPath ) )
250303 {
251- var start = index + nowListeningOnLength ;
252- var uri = output . Substring ( start , output . Length - start ) . Trim ( ) ;
253- port = new Uri ( uri ) . Port ;
254- return true ;
304+ return envPath ;
255305 }
256306
257- port = null ;
258- return false ;
307+ return null ;
259308 }
260309
261310 /// <summary>
@@ -278,15 +327,6 @@ public virtual async Task CheckProxyOutputAsync()
278327 }
279328 }
280329
281- CheckForErrors ( ) ;
282- }
283-
284- /// <summary>
285- /// Checks for any errors in the error buffer and throws an exception if errors are found.
286- /// </summary>
287- /// <exception cref="InvalidOperationException">Thrown when errors are found in the test proxy.</exception>
288- private void CheckForErrors ( )
289- {
290330 if ( _errorBuffer . Length > 0 )
291331 {
292332 var error = _errorBuffer . ToString ( ) ;
0 commit comments