Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ namespace Microsoft.Mcp.Tests.Client;
public sealed class TestProxy(bool debug = false) : IDisposable
{
private readonly bool _debug = debug;
public StringBuilder stderr = new();
public readonly StringBuilder stdout = new();
private StringBuilder _stderr = new();
private readonly StringBuilder _stdout = new();
private bool _started;
private Process? _process;
private CancellationTokenSource? _cts;
private int? _httpPort;
Expand All @@ -34,19 +35,19 @@ public sealed class TestProxy(bool debug = false) : IDisposable
private static string? _cachedRootDir;
private static string? _cachedExecutable;
private static string? _cachedVersion;
private static readonly TimeSpan[] DownloadRetryDelays = new[]
{
private static readonly TimeSpan[] s_downloadRetryDelays =
[
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(60)
};
];

/// <summary>
/// In-process synchronization lock to avoid proxy exe mismanagement.
/// </summary>
private static readonly SemaphoreSlim s_downloadLock = new(1, 1);

private async Task<string> EnsureProxyExecutableAsync(string repositoryRoot, string assetsJsonPath)
private static async Task<string> EnsureProxyExecutableAsync(string repositoryRoot, string assetsJsonPath)
{
if (_cachedExecutable != null)
{
Expand Down Expand Up @@ -89,7 +90,7 @@ private async Task<string> EnsureProxyExecutableAsync(string repositoryRoot, str
return _cachedExecutable;
}

private async Task EnsureProxyRecordings(string proxyExe, string repositoryRoot, string assetsJsonPath)
private static async Task EnsureProxyRecordings(string proxyExe, string repositoryRoot, string assetsJsonPath)
{
await s_downloadLock.WaitAsync().ConfigureAwait(false);
FileStream? lockStream = null;
Expand All @@ -107,7 +108,7 @@ private async Task EnsureProxyRecordings(string proxyExe, string repositoryRoot,
}
}

private async Task DownloadProxyAsync(string proxyDirectory, string version)
private static async Task DownloadProxyAsync(string proxyDirectory, string version)
{
var assetName = GetAssetNameForPlatform();
var url = $"https://github.com/Azure/azure-sdk-tools/releases/download/Azure.Sdk.Tools.TestProxy_{version}/{assetName}";
Expand Down Expand Up @@ -151,9 +152,9 @@ private static async Task<byte[]> DownloadWithRetryAsync(HttpClient client, stri
{
return await client.GetByteArrayAsync(url).ConfigureAwait(false);
}
catch when (attempt < DownloadRetryDelays.Length)
catch when (attempt < s_downloadRetryDelays.Length)
{
var delay = DownloadRetryDelays[attempt];
var delay = s_downloadRetryDelays[attempt];
await Task.Delay(delay).ConfigureAwait(false);
attempt++;
}
Expand Down Expand Up @@ -219,7 +220,7 @@ private static async Task<FileStream> AcquireDownloadLockAsync(string proxyDirec
}
}

private bool CheckProxyVersion(string proxyDirectory, string version)
private static bool CheckProxyVersion(string proxyDirectory, string version)
{
var versionFilePath = Path.Combine(proxyDirectory, "version.txt");
if (File.Exists(versionFilePath))
Expand All @@ -233,7 +234,7 @@ private bool CheckProxyVersion(string proxyDirectory, string version)
return false;
}

private string GetAssetNameForPlatform()
private static string GetAssetNameForPlatform()
{
var arch = RuntimeInformation.ProcessArchitecture;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Expand All @@ -247,7 +248,7 @@ private string GetAssetNameForPlatform()
return (arch == Architecture.Arm64 ? "test-proxy-standalone-linux-arm64.tar.gz" : "test-proxy-standalone-linux-x64.tar.gz");
}

private string FindExecutableInDirectory(string dir)
private static string FindExecutableInDirectory(string dir)
{
var exeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Azure.Sdk.Tools.TestProxy.exe" : "Azure.Sdk.Tools.TestProxy";
foreach (var file in Directory.EnumerateFiles(dir, exeName, SearchOption.AllDirectories))
Expand All @@ -261,7 +262,7 @@ private string FindExecutableInDirectory(string dir)
throw new FileNotFoundException($"Could not find {exeName} in {dir}");
}

private void EnsureExecutable(string path)
private static void EnsureExecutable(string path)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Expand All @@ -275,7 +276,7 @@ private void EnsureExecutable(string path)
}
}

private string GetRootDirectory()
private static string GetRootDirectory()
{
if (_cachedRootDir != null)
{
Expand All @@ -296,7 +297,7 @@ private string GetRootDirectory()
throw new InvalidOperationException("Could not find repository root (.git)");
}

private string GetTargetVersion()
private static string GetTargetVersion()
{
if (_cachedVersion != null)
{
Expand All @@ -312,7 +313,7 @@ private string GetTargetVersion()
return _cachedVersion;
}

private string GetProxyDirectory()
private static string GetProxyDirectory()
{
var root = GetRootDirectory();
var proxyDirectory = Path.Combine(root, ".proxy");
Expand All @@ -325,61 +326,77 @@ private string GetProxyDirectory()

public async Task Start(string repositoryRoot, string assetsJsonPath)
{
if (_process != null)
if (_started)
{
return;
}

var proxyExe = await EnsureProxyExecutableAsync(repositoryRoot, assetsJsonPath).ConfigureAwait(false);
await EnsureProxyRecordings(proxyExe, repositoryRoot, assetsJsonPath).ConfigureAwait(false);

if (string.IsNullOrWhiteSpace(proxyExe) || !File.Exists(proxyExe))
// If we're not running in CI, start a new Test Proxy executable.
if (RunningLocally())
{
throw new InvalidOperationException("Unable to locate test-proxy executable.");
}
var proxyExe = await EnsureProxyExecutableAsync(repositoryRoot, assetsJsonPath).ConfigureAwait(false);
await EnsureProxyRecordings(proxyExe, repositoryRoot, assetsJsonPath).ConfigureAwait(false);

var storageLocation = Environment.GetEnvironmentVariable("TEST_PROXY_STORAGE") ?? repositoryRoot;
var args = $"start --http-proxy --storage-location=\"{storageLocation}\"";
if (string.IsNullOrWhiteSpace(proxyExe) || !File.Exists(proxyExe))
{
throw new InvalidOperationException("Unable to locate test-proxy executable.");
}

ProcessStartInfo psi = new(proxyExe, args);
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.UseShellExecute = false;
psi.EnvironmentVariables["ASPNETCORE_URLS"] = "http://127.0.0.1:0"; // Let proxy choose free port
var storageLocation = Environment.GetEnvironmentVariable("TEST_PROXY_STORAGE") ?? repositoryRoot;
var args = $"start --http-proxy --storage-location=\"{storageLocation}\"";

_process = Process.Start(psi);
ProcessStartInfo psi = new(proxyExe, args)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
psi.EnvironmentVariables["ASPNETCORE_URLS"] = "http://127.0.0.1:0"; // Let proxy choose free port
if (_debug)
{
psi.EnvironmentVariables["LOGGING__LEVEL"] = "Debug";
psi.EnvironmentVariables["LOGGING__LOGLEVEL__MICROSOFT"] = "true";
psi.EnvironmentVariables["LOGGING__LOGLEVEL__DEFAULT"] = "true";
}

if (_process == null)
{
throw new InvalidOperationException("Failed to start test proxy process.");
}
_cts = new CancellationTokenSource();
_ = Task.Run(() => _pumpAsync(_process.StandardError, stderr, _cts.Token));
_ = Task.Run(() => _pumpAsync(_process.StandardOutput, stdout, _cts.Token));
_process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start test proxy process.");
_cts = new CancellationTokenSource();
_ = Task.Run(() => _pumpAsync(_process.StandardError, _stderr, _cts.Token));
_ = Task.Run(() => _pumpAsync(_process.StandardOutput, _stdout, _cts.Token));

if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("PROXY_MANUAL_START")))
{
_httpPort = 5000;
}
else
{
var secondsToWait = 30;
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CI")) || string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TF_BUILD")))
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("PROXY_MANUAL_START")))
{
secondsToWait = 90;
_httpPort = 5000;
}
else
{
var secondsToWait = 30;
if (RunningLocally())
{
secondsToWait = 90;
}
_httpPort = _waitForHttpPort(TimeSpan.FromSeconds(secondsToWait));
}
_httpPort = _waitForHttpPort(TimeSpan.FromSeconds(secondsToWait));
}

if (_httpPort is null)
if (_httpPort is null)
{
throw new InvalidOperationException($"Failed to detect test-proxy HTTP port. Output: {_stdout}\nErrors: {_stderr}");
}
}
else
{
throw new InvalidOperationException($"Failed to detect test-proxy HTTP port. Output: {stdout}\nErrors: {stderr}");
_httpPort = 5000;
}

Client = new TestProxyClient(new Uri(BaseUri), new TestProxyClientOptions());
AdminClient = Client.GetTestProxyAdminClient();
_started = true;
}
Comment thread
alzimmermsft marked this conversation as resolved.

private static bool RunningLocally() =>
string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CI")) ||
string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TF_BUILD"));

private static async Task _pumpAsync(StreamReader reader, StringBuilder sink, CancellationToken ct)
{
try
Expand All @@ -404,9 +421,9 @@ private static async Task _pumpAsync(StreamReader reader, StringBuilder sink, Ca
while ((DateTime.UtcNow - start) < timeout)
{
string text;
lock (stdout)
lock (_stdout)
{
text = stdout.ToString();
text = _stdout.ToString();
}
foreach (var line in text.Split('\n'))
{
Expand Down Expand Up @@ -441,11 +458,11 @@ private static bool _tryParsePort(string line, out int port)
/// <returns></returns>
public string? SnapshotStdErr()
{
lock (stderr)
lock (_stderr)
{
var toOutput = stderr.Length == 0 ? null : stderr.ToString();
var toOutput = _stderr.Length == 0 ? null : _stderr.ToString();

stderr = new();
_stderr = new();

return toOutput;
Comment thread
alzimmermsft marked this conversation as resolved.
}
Expand Down
28 changes: 28 additions & 0 deletions eng/pipelines/templates/jobs/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ jobs:

- template: /eng/pipelines/templates/steps/install-dotnet.yml

- template: /eng/common/testproxy/test-proxy-tool.yml
parameters:
runProxy: true

Comment thread
alzimmermsft marked this conversation as resolved.
- task: Powershell@2
displayName: "Build code"
condition: and(succeeded(), ne(variables['NoPackagesChanged'],'true'))
Expand Down Expand Up @@ -127,3 +131,27 @@ jobs:
parameters:
ArtifactPath: $(Build.ArtifactStagingDirectory)
ArtifactName: binaries_$(System.JobName)

# Shut down proxy to prevent file locks on the log file for auto-injected credscan
# steps on windows
- template: /eng/common/testproxy/test-proxy-tool-shutdown.yml

Comment thread
alzimmermsft marked this conversation as resolved.
- pwsh: |
$files = Get-ChildItem -Path $(Build.SourcesDirectory) -Filter test-proxy.log
foreach($file in $files){
Write-Host "##[group]$file"
cat $file
Write-Host "##[endgroup]"
}
displayName: Dump Test-Proxy Logs
condition: and(succeededOrFailed(), eq(variables['RunRecordedTests'], 'true'))

- pwsh: |
$files = Get-ChildItem -Path $(Build.SourcesDirectory) -Filter test-proxy-error.log
foreach($file in $files){
Write-Host "##[group]$file"
cat $file
Write-Host "##[endgroup]"
}
displayName: Dump Test-Proxy Error Logs
condition: and(succeededOrFailed(), eq(variables['RunRecordedTests'], 'true'))
Loading