From 511e56a379753b371a014c77a9ddc162a2c05179 Mon Sep 17 00:00:00 2001 From: Maddy Redding Heaps Date: Tue, 14 Oct 2025 15:59:48 -0700 Subject: [PATCH 01/16] add dotnet checks --- .../DotNetLanguageSpecificChecksTests.cs | 426 ++++++++++++++++++ .../Languages/DotNetLanguageSpecificChecks.cs | 190 +++++++- 2 files changed, 610 insertions(+), 6 deletions(-) create mode 100644 tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs new file mode 100644 index 00000000000..d9205e18b56 --- /dev/null +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs @@ -0,0 +1,426 @@ +using Azure.Sdk.Tools.Cli.Helpers; +using Azure.Sdk.Tools.Cli.Services; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; + +namespace Azure.Sdk.Tools.Cli.Tests.Services.Languages; + +[TestFixture] +internal class DotNetLanguageSpecificChecksTests +{ + private Mock _processHelperMock = null!; + private Mock _gitHelperMock = null!; + private DotNetLanguageSpecificChecks _languageChecks = null!; + private string _packagePath = null!; + private string _repoRoot = null!; + private const string RequiredDotNetVersion = "9.0.102"; + + [SetUp] + public void SetUp() + { + _processHelperMock = new Mock(); + _gitHelperMock = new Mock(); + + _languageChecks = new DotNetLanguageSpecificChecks( + _processHelperMock.Object, + _gitHelperMock.Object, + NullLogger.Instance); + + _repoRoot = Path.Combine(Path.GetTempPath(), "azure-sdk-for-net"); + _packagePath = Path.Combine(_repoRoot, "sdk", "storage", "Azure.Storage.Blobs"); + } + + private void SetupSuccessfulDotNetVersionCheck() + { + var versionOutput = $"9.0.100 [C:\\Program Files\\dotnet\\sdk]\n{RequiredDotNetVersion} [C:\\Program Files\\dotnet\\sdk]"; + var processResult = new ProcessResult { ExitCode = 0 }; + processResult.AppendStdout(versionOutput); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + p.Command == "cmd.exe" && + p.Args.Contains("/C") && + p.Args.Contains("dotnet") && + p.Args.Contains("--list-sdks")), + It.IsAny())) + .ReturnsAsync(processResult); + } + + private void SetupGitRepoDiscovery() + { + _gitHelperMock + .Setup(x => x.DiscoverRepoRoot(It.IsAny())) + .Returns(_repoRoot); + } + + [Test] + public async Task TestPackCodeAsyncDotNetNotInstalledReturnsError() + { + var processResult = new ProcessResult { ExitCode = 1 }; + processResult.AppendStderr("dotnet command not found"); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + p.Command == "cmd.exe" && + p.Args.Contains("/C") && + p.Args.Contains("dotnet") && + p.Args.Contains("--list-sdks")), + It.IsAny())) + .ReturnsAsync(processResult); + + var result = await _languageChecks.PackCodeAsync(_packagePath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.CheckStatusDetails, Does.Contain("dotnet --list-sdks failed")); + }); + + _processHelperMock.Verify( + x => x.Run( + It.Is(p => p.Args.Contains("pack")), + It.IsAny()), + Times.Never); + } + + [Test] + public async Task TestPackCodeAsyncOldDotNetVersionReturnsError() + { + var versionOutput = "8.0.100 [C:\\Program Files\\dotnet\\sdk]\n8.0.200 [C:\\Program Files\\dotnet\\sdk]"; + var processResult = new ProcessResult { ExitCode = 0 }; + processResult.AppendStdout(versionOutput); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + p.Command == "cmd.exe" && + p.Args.Contains("/C") && + p.Args.Contains("dotnet") && + p.Args.Contains("--list-sdks")), + It.IsAny())) + .ReturnsAsync(processResult); + + var result = await _languageChecks.PackCodeAsync(_packagePath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.ResponseError, Does.Contain("below minimum requirement")); + Assert.That(result.ResponseError, Does.Contain(RequiredDotNetVersion)); + }); + } + + [Test] + public async Task TestPackCodeAsyncInvalidPackagePathReturnsError() + { + SetupSuccessfulDotNetVersionCheck(); + var invalidPath = "/tmp/not-in-sdk-folder"; + + var result = await _languageChecks.PackCodeAsync(invalidPath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.ResponseError, Does.Contain("Failed to determine service directory")); + }); + } + + [Test] + public async Task TestPackCodeAsyncSuccessReturnsSuccess() + { + SetupSuccessfulDotNetVersionCheck(); + + var processResult = new ProcessResult { ExitCode = 0 }; + processResult.AppendStdout( + "Microsoft (R) Build Engine version 17.10.0+1\n" + + "Build started 1/9/2025 10:30:00 AM.\n" + + " Determining projects to restore...\n" + + " All projects are up-to-date for restore.\n" + + " Azure.Storage.Blobs -> bin/Release/net8.0/Azure.Storage.Blobs.dll\n" + + " Successfully created package 'bin/Release/Azure.Storage.Blobs.1.0.0.nupkg'.\n" + + "Build succeeded.\n" + + " 0 Warning(s)\n" + + " 0 Error(s)" + ); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + p.Command == "cmd.exe" && + p.Args.Contains("/C") && + p.Args.Contains("dotnet") && + p.Args.Contains("pack") && + p.Args.Any(a => a.Contains("service.proj"))), + It.IsAny())) + .ReturnsAsync(processResult); + + SetupGitRepoDiscovery(); + + var result = await _languageChecks.PackCodeAsync(_packagePath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(0)); + Assert.That(result.CheckStatusDetails, Does.Contain("Build succeeded")); + }); + + _processHelperMock.Verify( + x => x.Run( + It.Is(p => + p.Command == "cmd.exe" && + p.Args.Contains("/C") && + p.Args.Contains("dotnet") && + p.Args.Contains("pack") && + p.Args.Any(a => a.Contains("service.proj")) && + p.Args.Contains("-warnaserror") && + p.Args.Contains("/p:ValidateRunApiCompat=true") && + p.Args.Contains("/p:SDKType=client") && + p.Args.Contains("/p:ServiceDirectory=storage") && + p.Args.Contains("/p:IncludeTests=false") && + p.Args.Contains("/p:PublicSign=false") && + p.Args.Contains("/p:Configuration=Release") && + p.Args.Contains("/p:IncludePerf=false") && + p.Args.Contains("/p:IncludeStress=false") && + p.Args.Contains("/p:IncludeIntegrationTests=false") && + p.WorkingDirectory == _repoRoot && + p.Timeout == TimeSpan.FromMinutes(10)), + It.IsAny()), + Times.Once); + } + + [Test] + public async Task TestPackCodeAsyncBuildFailureReturnsError() + { + SetupSuccessfulDotNetVersionCheck(); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + p.Command == "cmd.exe" && + p.Args.Contains("/C") && + p.Args.Contains("dotnet") && + p.Args.Contains("pack")), + It.IsAny())) + .ReturnsAsync(() => + { + var processResult = new ProcessResult { ExitCode = 1 }; + processResult.AppendStderr("error CS0246: The type or namespace name 'Azure' could not be found"); + return processResult; + }); + + SetupGitRepoDiscovery(); + + var result = await _languageChecks.PackCodeAsync(_packagePath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.CheckStatusDetails, Does.Contain("could not be found")); + }); + } + + [Test] + public async Task TestCheckGeneratedCodeAsyncDotNetNotInstalledReturnsError() + { + var processResult = new ProcessResult { ExitCode = 1 }; + processResult.AppendStderr("dotnet command not found"); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + p.Command == "cmd.exe" && + p.Args.Contains("/C") && + p.Args.Contains("dotnet") && + p.Args.Contains("--list-sdks")), + It.IsAny())) + .ReturnsAsync(processResult); + + var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.CheckStatusDetails, Does.Contain("dotnet --list-sdks failed")); + }); + } + + [Test] + public async Task TestCheckGeneratedCodeAsyncInvalidPackagePathReturnsError() + { + SetupSuccessfulDotNetVersionCheck(); + var invalidPath = "/tmp/not-in-sdk-folder"; + + var result = await _languageChecks.CheckGeneratedCodeAsync(invalidPath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.ResponseError, Does.Contain("Failed to determine service directory")); + }); + } + + [Test] + public async Task TestCheckGeneratedCodeAsyncSuccessReturnsSuccess() + { + SetupSuccessfulDotNetVersionCheck(); + SetupGitRepoDiscovery(); + + var processResult = new ProcessResult { ExitCode = 0 }; + processResult.AppendStdout("All checks passed successfully!"); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + p.Command == "pwsh" && + p.Args.Any(a => a.Contains("CodeChecks.ps1"))), + It.IsAny())) + .ReturnsAsync(processResult); + + var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(0)); + Assert.That(result.CheckStatusDetails, Does.Contain("All checks passed successfully")); + }); + } + + [TestCase(true)] + [TestCase(false)] + [Test] + public async Task TestCheckGeneratedCodeAsyncSpellCheckFailureReturnsError(bool isGeneratedCode) + { + + var errorMessage = isGeneratedCode ? + "Generated code does not match committed code. Please regenerate and commit." : + "Spell check failed: 'BlobClinet' is misspelled. Did you mean 'BlobClient'?"; + + var expectedDetail = isGeneratedCode ? + "Generated code does not match" : + "Spell check failed"; + + SetupSuccessfulDotNetVersionCheck(); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + p.Command == "pwsh" && + p.Args.Any(a => a.Contains("CodeChecks.ps1"))), + It.IsAny())) + .ReturnsAsync(() => + { + var processResult = new ProcessResult { ExitCode = 1 }; + processResult.AppendStderr(errorMessage); + return processResult; + }); + + SetupGitRepoDiscovery(); + + var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.CheckStatusDetails, Does.Contain(expectedDetail)); + }); + } + + [Test] + public async Task TestCheckAotCompatAsyncDotNetNotInstalledReturnsError() + { + var processResult = new ProcessResult { ExitCode = 1 }; + processResult.AppendStderr("dotnet command not found"); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + p.Command == "cmd.exe" && + p.Args.Contains("/C") && + p.Args.Contains("dotnet") && + p.Args.Contains("--list-sdks")), + It.IsAny())) + .ReturnsAsync(processResult); + + var result = await _languageChecks.CheckAotCompatAsync(_packagePath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.CheckStatusDetails, Does.Contain("dotnet --list-sdks failed")); + }); + } + + [Test] + public async Task TestCheckAotCompatAsyncInvalidPackagePathReturnsError() + { + SetupSuccessfulDotNetVersionCheck(); + var invalidPath = "/tmp/not-in-sdk-folder"; + + var result = await _languageChecks.CheckAotCompatAsync(invalidPath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.ResponseError, Does.Contain("Failed to determine service directory or package name")); + }); + } + + [Test] + public async Task TestCheckAotCompatAsyncSuccessReturnsSuccess() + { + SetupSuccessfulDotNetVersionCheck(); + SetupGitRepoDiscovery(); + + var processResult = new ProcessResult { ExitCode = 0 }; + processResult.AppendStdout("AOT compatibility check passed!"); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + p.Command == "pwsh" && + p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), + It.IsAny())) + .ReturnsAsync(processResult); + + var result = await _languageChecks.CheckAotCompatAsync(_packagePath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(0)); + Assert.That(result.CheckStatusDetails, Does.Contain("AOT compatibility check passed")); + }); + } + + [Test] + public async Task TestCheckAotCompatAsyncAotWarningsReturnsError() + { + SetupSuccessfulDotNetVersionCheck(); + SetupGitRepoDiscovery(); + + var errorMessage = "ILLink : Trim analysis warning IL2026: Azure.Storage.Blobs.BlobClient.Upload: " + + "Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' " + + "can break functionality when trimming application code."; + + var processResult = new ProcessResult { ExitCode = 1 }; + processResult.AppendStderr(errorMessage); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + p.Command == "pwsh" && + p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), + It.IsAny())) + .ReturnsAsync(processResult); + + var result = await _languageChecks.CheckAotCompatAsync(_packagePath, CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.CheckStatusDetails, Does.Contain("Trim analysis warning")); + Assert.That(result.CheckStatusDetails, Does.Contain("RequiresUnreferencedCodeAttribute")); + }); + } +} diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs index d799ab3898c..eee530bc280 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs @@ -1,7 +1,5 @@ -using System.Runtime.InteropServices; -using Azure.Sdk.Tools.Cli.Models; using Azure.Sdk.Tools.Cli.Helpers; -using Microsoft.Extensions.Logging; +using Azure.Sdk.Tools.Cli.Models; namespace Azure.Sdk.Tools.Cli.Services; @@ -12,19 +10,199 @@ namespace Azure.Sdk.Tools.Cli.Services; public class DotNetLanguageSpecificChecks : ILanguageSpecificChecks { private readonly IProcessHelper _processHelper; - private readonly INpxHelper _npxHelper; private readonly IGitHelper _gitHelper; private readonly ILogger _logger; + private const string DotNetCommand = "dotnet"; + private const string PowerShellCommand = "pwsh"; + private const string RequiredDotNetVersion = "9.0.102"; public DotNetLanguageSpecificChecks( IProcessHelper processHelper, - INpxHelper npxHelper, IGitHelper gitHelper, ILogger logger) { _processHelper = processHelper; - _npxHelper = npxHelper; _gitHelper = gitHelper; _logger = logger; } + + public async Task PackCodeAsync(string packagePath, CancellationToken cancellationToken = default) + { + try + { + var dotnetVersionValidation = await VerifyDotnetVersion(); + if (dotnetVersionValidation.ExitCode != 0) + { + return dotnetVersionValidation; + } + + var serviceDirectory = GetServiceDirectoryFromPath(packagePath); + if (serviceDirectory == null) + { + return new CLICheckResponse(1, "", "Failed to determine service directory from the provided package path."); + } + + var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); + var serviceProj = Path.Combine(repoRoot, "eng", "service.proj"); + + // Run dotnet pack with the same parameters as CI + var args = new List + { + "pack", + serviceProj, + "-warnaserror", + "/p:ValidateRunApiCompat=true", + "/p:SDKType=client", + $"/p:ServiceDirectory={serviceDirectory}", + "/p:IncludeTests=false", + "/p:PublicSign=false", + "/p:Configuration=Release", + "/p:IncludePerf=false", + "/p:IncludeStress=false", + "/p:IncludeIntegrationTests=false" + }; + + var timeout = TimeSpan.FromMinutes(10); + var result = await _processHelper.Run(new ProcessOptions(DotNetCommand, args.ToArray(), timeout: timeout, workingDirectory: repoRoot), cancellationToken); + return new CLICheckResponse(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "{MethodName} failed with an exception", nameof(PackCodeAsync)); + return new CLICheckResponse(1, "", $"{nameof(PackCodeAsync)} failed with an exception: {ex.Message}"); + } + } + + public async Task CheckAotCompatAsync(string packagePath, CancellationToken cancellationToken = default) + { + try + { + var dotnetVersionValidation = await VerifyDotnetVersion(); + if (dotnetVersionValidation.ExitCode != 0) + { + return dotnetVersionValidation; + } + + var serviceDirectory = GetServiceDirectoryFromPath(packagePath); + var packageName = GetPackageNameFromPath(packagePath); + if (serviceDirectory == null || packageName == null) + { + return new CLICheckResponse(1, "", "Failed to determine service directory or package name from the provided package path."); + } + var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); + var scriptPath = Path.Combine(repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); + var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-PackageName", packageName }; + var timeout = TimeSpan.FromMinutes(6); + var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: repoRoot), cancellationToken); + + return result.ExitCode switch + { + 0 => new CLICheckResponse(result.ExitCode, result.Output), + _ => new CLICheckResponse(result.ExitCode, result.Output, "Process failed"), + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "{MethodName} failed with an exception", nameof(CheckAotCompatAsync)); + return new CLICheckResponse(1, "", $"{nameof(CheckAotCompatAsync)} failed with an exception: {ex.Message}"); + } + } + + public async Task CheckGeneratedCodeAsync(string packagePath, CancellationToken cancellationToken = default) + { + try + { + var dotnetVersionValidation = await VerifyDotnetVersion(); + if (dotnetVersionValidation.ExitCode != 0) + { + return dotnetVersionValidation; + } + var serviceDirectory = GetServiceDirectoryFromPath(packagePath); + if (serviceDirectory == null) + { + return new CLICheckResponse(1, "", "Failed to determine service directory from the provided package path."); + } + var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); + var scriptPath = Path.Combine(repoRoot, "eng", "scripts", "CodeChecks.ps1"); + var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-SpellCheckPublicApiSurface" }; + var timeout = TimeSpan.FromMinutes(6); + var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: repoRoot), cancellationToken); + + return result.ExitCode switch + { + 0 => new CLICheckResponse(result.ExitCode, result.Output), + _ => new CLICheckResponse(result.ExitCode, result.Output, "Process failed"), + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "{MethodName} failed with an exception", nameof(CheckGeneratedCodeAsync)); + return new CLICheckResponse(1, "", $"{nameof(CheckGeneratedCodeAsync)} failed with an exception: {ex.Message}"); + } + } + + private async ValueTask VerifyDotnetVersion() + { + var dotnetSDKCheck = await _processHelper.Run(new ProcessOptions(DotNetCommand, ["--list-sdks"]), CancellationToken.None); + if (dotnetSDKCheck.ExitCode != 0) + { + return new CLICheckResponse(dotnetSDKCheck.ExitCode, $"dotnet --list-sdks failed with an error: {dotnetSDKCheck.Output}"); + } + + var dotnetVersions = dotnetSDKCheck.Output.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); + var latestVersionNumber = dotnetVersions[dotnetVersions.Length - 1].Split('[')[0].Trim(); + + if (Version.TryParse(latestVersionNumber, out var installedVersion) && + Version.TryParse(RequiredDotNetVersion, out var minimumVersion)) + { + if (installedVersion >= minimumVersion) + { + return new CLICheckResponse(0, $".NET SDK version {latestVersionNumber} meets minimum requirement of {RequiredDotNetVersion}"); + } + else + { + return new CLICheckResponse(1, "", $".NET SDK version {latestVersionNumber} is below minimum requirement of {RequiredDotNetVersion}"); + } + } + else + { + return new CLICheckResponse(1, "", $"Failed to parse .NET SDK version: {latestVersionNumber}"); + } + } + + private string? GetServiceDirectoryFromPath(string packagePath) + { + string? serviceDirectory = null; + var normalizedPath = packagePath.Replace('\\', '/'); + var sdkIndex = normalizedPath.IndexOf("/sdk/", StringComparison.OrdinalIgnoreCase); + + if (sdkIndex >= 0) + { + var pathAfterSdk = normalizedPath.Substring(sdkIndex + 5); // Skip "/sdk/" + var segments = pathAfterSdk.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length > 0) + { + serviceDirectory = segments[0]; + } + } + return serviceDirectory; + } + + private string? GetPackageNameFromPath(string packagePath) + { + string? packageName = null; + var normalizedPath = packagePath.Replace('\\', '/'); + var sdkIndex = normalizedPath.IndexOf("/sdk/", StringComparison.OrdinalIgnoreCase); + + if (sdkIndex >= 0) + { + var pathAfterSdk = normalizedPath.Substring(sdkIndex + 5); // Skip "/sdk/" + var segments = pathAfterSdk.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length > 1) + { + packageName = segments[1]; + } + } + return packageName; + } } From 212de8ef2f7460b33c2e1b2fd8822ae911620eea Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Tue, 14 Oct 2025 16:31:18 -0700 Subject: [PATCH 02/16] add integration --- .../DotNetLanguageSpecificChecksTests.cs | 8 +- .../Languages/DotNetLanguageSpecificChecks.cs | 6 +- .../Languages/ILanguageSpecificChecks.cs | 33 ++++++++ .../Services/Languages/LanguageChecks.cs | 75 +++++++++++++++++++ .../Tools/Package/PackageCheckTool.cs | 27 +++++++ 5 files changed, 142 insertions(+), 7 deletions(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs index d9205e18b56..7f52522e46f 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs @@ -237,7 +237,7 @@ public async Task TestCheckGeneratedCodeAsyncDotNetNotInstalledReturnsError() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.RunGeneratedCodeChecksAsync(_packagePath, CancellationToken.None); Assert.Multiple(() => { @@ -252,7 +252,7 @@ public async Task TestCheckGeneratedCodeAsyncInvalidPackagePathReturnsError() SetupSuccessfulDotNetVersionCheck(); var invalidPath = "/tmp/not-in-sdk-folder"; - var result = await _languageChecks.CheckGeneratedCodeAsync(invalidPath, CancellationToken.None); + var result = await _languageChecks.RunGeneratedCodeChecksAsync(invalidPath, CancellationToken.None); Assert.Multiple(() => { @@ -278,7 +278,7 @@ public async Task TestCheckGeneratedCodeAsyncSuccessReturnsSuccess() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.RunGeneratedCodeChecksAsync(_packagePath, CancellationToken.None); Assert.Multiple(() => { @@ -318,7 +318,7 @@ public async Task TestCheckGeneratedCodeAsyncSpellCheckFailureReturnsError(bool SetupGitRepoDiscovery(); - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.RunGeneratedCodeChecksAsync(_packagePath, CancellationToken.None); Assert.Multiple(() => { diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs index eee530bc280..643c1a3182d 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs @@ -108,7 +108,7 @@ public async Task CheckAotCompatAsync(string packagePath, Canc } } - public async Task CheckGeneratedCodeAsync(string packagePath, CancellationToken cancellationToken = default) + public async Task RunGeneratedCodeChecksAsync(string packagePath, CancellationToken cancellationToken = default) { try { @@ -136,8 +136,8 @@ public async Task CheckGeneratedCodeAsync(string packagePath, } catch (Exception ex) { - _logger.LogError(ex, "{MethodName} failed with an exception", nameof(CheckGeneratedCodeAsync)); - return new CLICheckResponse(1, "", $"{nameof(CheckGeneratedCodeAsync)} failed with an exception: {ex.Message}"); + _logger.LogError(ex, "{MethodName} failed with an exception", nameof(RunGeneratedCodeChecksAsync)); + return new CLICheckResponse(1, "", $"{nameof(RunGeneratedCodeChecksAsync)} failed with an exception: {ex.Message}"); } } diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs index 96ef3c20d5e..98a8df45be1 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs @@ -54,4 +54,37 @@ Task FormatCodeAsync(string packagePath, bool fixCheckErrors = { return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); } + + /// + /// Packs code in the specific package using language-specific tools. + /// + /// Path to the package directory + /// Cancellation token + /// Result of the code packing operation + Task PackCodeAsync(string packagePath, CancellationToken cancellationToken = default) + { + return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); + } + + /// + /// Checks AOT compatibility for the specific package using language-specific tools. + /// + /// Path to the package directory + /// Cancellation token + /// Result of the AOT compatibility check + Task CheckAotCompatAsync(string packagePath, CancellationToken cancellationToken = default) + { + return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); + } + + /// + /// Checks generated code for the specific package using language-specific tools. + /// + /// Path to the package directory + /// Cancellation token + /// Result of the generated code check + Task CheckGeneratedCodeAsync(string packagePath, CancellationToken cancellationToken = default) + { + return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); + } } \ No newline at end of file diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs index 70fe5aae558..b5e01ad28be 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs @@ -77,6 +77,30 @@ public interface ILanguageChecks /// Result of the code formatting operation Task FormatCodeAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default); + /// + /// Packs code in the specific package. + /// + /// Path to the package directory + /// Cancellation token + /// Result of the code packing operation + Task PackCodeAsync(string packagePath, CancellationToken ct = default); + + /// + /// Checks AOT compatibility for the specific package. + /// + /// Path to the package directory + /// Cancellation token + /// Result of the AOT compatibility check + Task CheckAotCompatAsync(string packagePath, CancellationToken ct = default); + + /// + /// Checks generated code for the specific package. + /// + /// Path to the package directory + /// Cancellation token + /// Result of the generated code check + Task CheckGeneratedCodeAsync(string packagePath, CancellationToken ct = default); + /// /// Gets the SDK package path for the given repository and package path. /// @@ -213,6 +237,57 @@ public virtual async Task FormatCodeAsync(string packagePath, return await languageSpecificCheck.FormatCodeAsync(packagePath, fixCheckErrors, ct); } + public virtual async Task PackCodeAsync(string packagePath, CancellationToken ct = default) + { + var languageSpecificCheck = await _languageSpecificChecks.Resolve(packagePath); + + if (languageSpecificCheck == null) + { + _logger.LogError("No language-specific check handler found for package at {PackagePath}. Supported languages may not include this package type.", packagePath); + return new CLICheckResponse( + exitCode: 1, + checkStatusDetails: $"No language-specific check handler found for package at {packagePath}. Supported languages may not include this package type.", + error: "Unsupported package type" + ); + } + + return await languageSpecificCheck.PackCodeAsync(packagePath, ct); + } + + public virtual async Task CheckAotCompatAsync(string packagePath, CancellationToken ct = default) + { + var languageSpecificCheck = await _languageSpecificChecks.Resolve(packagePath); + + if (languageSpecificCheck == null) + { + _logger.LogError("No language-specific check handler found for package at {PackagePath}. Supported languages may not include this package type.", packagePath); + return new CLICheckResponse( + exitCode: 1, + checkStatusDetails: $"No language-specific check handler found for package at {packagePath}. Supported languages may not include this package type.", + error: "Unsupported package type" + ); + } + + return await languageSpecificCheck.CheckAotCompatAsync(packagePath, ct); + } + + public virtual async Task CheckGeneratedCodeAsync(string packagePath, CancellationToken ct = default) + { + var languageSpecificCheck = await _languageSpecificChecks.Resolve(packagePath); + + if (languageSpecificCheck == null) + { + _logger.LogError("No language-specific check handler found for package at {PackagePath}. Supported languages may not include this package type.", packagePath); + return new CLICheckResponse( + exitCode: 1, + checkStatusDetails: $"No language-specific check handler found for package at {packagePath}. Supported languages may not include this package type.", + error: "Unsupported package type" + ); + } + + return await languageSpecificCheck.CheckGeneratedCodeAsync(packagePath, ct); + } + /// /// Common changelog validation implementation that works for most Azure SDK languages. /// Uses the PowerShell script from eng/common/scripts/Verify-ChangeLog.ps1. diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs index b7ec73e2246..37f6da0843c 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs @@ -184,6 +184,33 @@ private async Task RunAllChecks(string packagePath, bool fixCh failedChecks.Add("Format"); } + // Run code packing + var packCodeResult = await languageChecks.PackCodeAsync(packagePath, ct); + results.Add(packCodeResult); + if (packCodeResult.ExitCode != 0) + { + overallSuccess = false; + failedChecks.Add("Pack"); + } + + // Run AOT compatibility check + var aotCompatResult = await languageChecks.CheckAotCompatAsync(packagePath, ct); + results.Add(aotCompatResult); + if (aotCompatResult.ExitCode != 0) + { + overallSuccess = false; + failedChecks.Add("AOT Compatibility"); + } + + // Run generated code check + var generatedCodeResult = await languageChecks.CheckGeneratedCodeAsync(packagePath, ct); + results.Add(generatedCodeResult); + if (generatedCodeResult.ExitCode != 0) + { + overallSuccess = false; + failedChecks.Add("Generated Code"); + } + var message = overallSuccess ? "All checks completed successfully" : "Some checks failed"; var combinedOutput = string.Join("\n", results.Select(r => r.CheckStatusDetails)); From 7afca47c91e5673a315414f1c4f781d07be592ce Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Tue, 14 Oct 2025 16:35:20 -0700 Subject: [PATCH 03/16] add logging --- .../DotNetLanguageSpecificChecksTests.cs | 8 +- .../Languages/DotNetLanguageSpecificChecks.cs | 111 ++++++++++++++++-- 2 files changed, 104 insertions(+), 15 deletions(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs index 7f52522e46f..d9205e18b56 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs @@ -237,7 +237,7 @@ public async Task TestCheckGeneratedCodeAsyncDotNetNotInstalledReturnsError() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.RunGeneratedCodeChecksAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); Assert.Multiple(() => { @@ -252,7 +252,7 @@ public async Task TestCheckGeneratedCodeAsyncInvalidPackagePathReturnsError() SetupSuccessfulDotNetVersionCheck(); var invalidPath = "/tmp/not-in-sdk-folder"; - var result = await _languageChecks.RunGeneratedCodeChecksAsync(invalidPath, CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCodeAsync(invalidPath, CancellationToken.None); Assert.Multiple(() => { @@ -278,7 +278,7 @@ public async Task TestCheckGeneratedCodeAsyncSuccessReturnsSuccess() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.RunGeneratedCodeChecksAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); Assert.Multiple(() => { @@ -318,7 +318,7 @@ public async Task TestCheckGeneratedCodeAsyncSpellCheckFailureReturnsError(bool SetupGitRepoDiscovery(); - var result = await _languageChecks.RunGeneratedCodeChecksAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); Assert.Multiple(() => { diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs index 643c1a3182d..ed9c83a97ca 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs @@ -30,20 +30,29 @@ public async Task PackCodeAsync(string packagePath, Cancellati { try { + _logger.LogInformation("Starting code packing for .NET project at: {PackagePath}", packagePath); + var dotnetVersionValidation = await VerifyDotnetVersion(); if (dotnetVersionValidation.ExitCode != 0) { + _logger.LogError("Dotnet version validation failed"); return dotnetVersionValidation; } var serviceDirectory = GetServiceDirectoryFromPath(packagePath); if (serviceDirectory == null) { + _logger.LogError("Failed to determine service directory from package path: {PackagePath}", packagePath); return new CLICheckResponse(1, "", "Failed to determine service directory from the provided package path."); } + _logger.LogInformation("Determined service directory: {ServiceDirectory}", serviceDirectory); + var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); + _logger.LogInformation("Found repository root at: {RepoRoot}", repoRoot); + var serviceProj = Path.Combine(repoRoot, "eng", "service.proj"); + _logger.LogInformation("Using service project file: {ServiceProj}", serviceProj); // Run dotnet pack with the same parameters as CI var args = new List @@ -62,8 +71,19 @@ public async Task PackCodeAsync(string packagePath, Cancellati "/p:IncludeIntegrationTests=false" }; + _logger.LogInformation("Executing command: {Command} {Arguments}", DotNetCommand, string.Join(" ", args)); var timeout = TimeSpan.FromMinutes(10); var result = await _processHelper.Run(new ProcessOptions(DotNetCommand, args.ToArray(), timeout: timeout, workingDirectory: repoRoot), cancellationToken); + + if (result.ExitCode == 0) + { + _logger.LogInformation("Code packing completed successfully"); + } + else + { + _logger.LogWarning("Code packing failed with exit code {ExitCode}", result.ExitCode); + } + return new CLICheckResponse(result); } catch (Exception ex) @@ -77,9 +97,12 @@ public async Task CheckAotCompatAsync(string packagePath, Canc { try { + _logger.LogInformation("Starting AOT compatibility check for .NET project at: {PackagePath}", packagePath); + var dotnetVersionValidation = await VerifyDotnetVersion(); if (dotnetVersionValidation.ExitCode != 0) { + _logger.LogError("Dotnet version validation failed for AOT compatibility check"); return dotnetVersionValidation; } @@ -87,19 +110,40 @@ public async Task CheckAotCompatAsync(string packagePath, Canc var packageName = GetPackageNameFromPath(packagePath); if (serviceDirectory == null || packageName == null) { + _logger.LogError("Failed to determine service directory or package name from package path: {PackagePath}", packagePath); return new CLICheckResponse(1, "", "Failed to determine service directory or package name from the provided package path."); } + + _logger.LogInformation("Determined service directory: {ServiceDirectory}, package name: {PackageName}", serviceDirectory, packageName); + var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); + _logger.LogInformation("Found repository root at: {RepoRoot}", repoRoot); + var scriptPath = Path.Combine(repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); + _logger.LogInformation("Using AOT compatibility script: {ScriptPath}", scriptPath); + + if (!File.Exists(scriptPath)) + { + _logger.LogError("AOT compatibility script not found at: {ScriptPath}", scriptPath); + return new CLICheckResponse(1, "", $"AOT compatibility script not found at: {scriptPath}"); + } + var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-PackageName", packageName }; + _logger.LogInformation("Executing command: {Command} {Arguments}", PowerShellCommand, string.Join(" ", args)); + var timeout = TimeSpan.FromMinutes(6); var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: repoRoot), cancellationToken); - return result.ExitCode switch + if (result.ExitCode == 0) { - 0 => new CLICheckResponse(result.ExitCode, result.Output), - _ => new CLICheckResponse(result.ExitCode, result.Output, "Process failed"), - }; + _logger.LogInformation("AOT compatibility check completed successfully"); + return new CLICheckResponse(result.ExitCode, result.Output); + } + else + { + _logger.LogWarning("AOT compatibility check failed with exit code {ExitCode}", result.ExitCode); + return new CLICheckResponse(result.ExitCode, result.Output, "AOT compatibility check failed"); + } } catch (Exception ex) { @@ -108,64 +152,97 @@ public async Task CheckAotCompatAsync(string packagePath, Canc } } - public async Task RunGeneratedCodeChecksAsync(string packagePath, CancellationToken cancellationToken = default) + public async Task CheckGeneratedCodeAsync(string packagePath, CancellationToken cancellationToken = default) { try { + _logger.LogInformation("Starting generated code checks for .NET project at: {PackagePath}", packagePath); + var dotnetVersionValidation = await VerifyDotnetVersion(); if (dotnetVersionValidation.ExitCode != 0) { + _logger.LogError("Dotnet version validation failed for generated code checks"); return dotnetVersionValidation; } + var serviceDirectory = GetServiceDirectoryFromPath(packagePath); if (serviceDirectory == null) { + _logger.LogError("Failed to determine service directory from package path: {PackagePath}", packagePath); return new CLICheckResponse(1, "", "Failed to determine service directory from the provided package path."); } + + _logger.LogInformation("Determined service directory: {ServiceDirectory}", serviceDirectory); + var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); + _logger.LogInformation("Found repository root at: {RepoRoot}", repoRoot); + var scriptPath = Path.Combine(repoRoot, "eng", "scripts", "CodeChecks.ps1"); + _logger.LogInformation("Using code checks script: {ScriptPath}", scriptPath); + + if (!File.Exists(scriptPath)) + { + _logger.LogError("Code checks script not found at: {ScriptPath}", scriptPath); + return new CLICheckResponse(1, "", $"Code checks script not found at: {scriptPath}"); + } + var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-SpellCheckPublicApiSurface" }; + _logger.LogInformation("Executing command: {Command} {Arguments}", PowerShellCommand, string.Join(" ", args)); + var timeout = TimeSpan.FromMinutes(6); var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: repoRoot), cancellationToken); - return result.ExitCode switch + if (result.ExitCode == 0) { - 0 => new CLICheckResponse(result.ExitCode, result.Output), - _ => new CLICheckResponse(result.ExitCode, result.Output, "Process failed"), - }; + _logger.LogInformation("Generated code checks completed successfully"); + return new CLICheckResponse(result.ExitCode, result.Output); + } + else + { + _logger.LogWarning("Generated code checks failed with exit code {ExitCode}", result.ExitCode); + return new CLICheckResponse(result.ExitCode, result.Output, "Generated code checks failed"); + } } catch (Exception ex) { - _logger.LogError(ex, "{MethodName} failed with an exception", nameof(RunGeneratedCodeChecksAsync)); - return new CLICheckResponse(1, "", $"{nameof(RunGeneratedCodeChecksAsync)} failed with an exception: {ex.Message}"); + _logger.LogError(ex, "{MethodName} failed with an exception", nameof(CheckGeneratedCodeAsync)); + return new CLICheckResponse(1, "", $"{nameof(CheckGeneratedCodeAsync)} failed with an exception: {ex.Message}"); } } private async ValueTask VerifyDotnetVersion() { + _logger.LogDebug("Verifying .NET SDK version"); + var dotnetSDKCheck = await _processHelper.Run(new ProcessOptions(DotNetCommand, ["--list-sdks"]), CancellationToken.None); if (dotnetSDKCheck.ExitCode != 0) { + _logger.LogError(".NET SDK is not installed or not available in PATH"); return new CLICheckResponse(dotnetSDKCheck.ExitCode, $"dotnet --list-sdks failed with an error: {dotnetSDKCheck.Output}"); } var dotnetVersions = dotnetSDKCheck.Output.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); var latestVersionNumber = dotnetVersions[dotnetVersions.Length - 1].Split('[')[0].Trim(); + _logger.LogInformation("Found .NET SDK version: {LatestVersion}", latestVersionNumber); + if (Version.TryParse(latestVersionNumber, out var installedVersion) && Version.TryParse(RequiredDotNetVersion, out var minimumVersion)) { if (installedVersion >= minimumVersion) { + _logger.LogInformation(".NET SDK version {InstalledVersion} meets minimum requirement of {RequiredVersion}", latestVersionNumber, RequiredDotNetVersion); return new CLICheckResponse(0, $".NET SDK version {latestVersionNumber} meets minimum requirement of {RequiredDotNetVersion}"); } else { + _logger.LogError(".NET SDK version {InstalledVersion} is below minimum requirement of {RequiredVersion}", latestVersionNumber, RequiredDotNetVersion); return new CLICheckResponse(1, "", $".NET SDK version {latestVersionNumber} is below minimum requirement of {RequiredDotNetVersion}"); } } else { + _logger.LogError("Failed to parse .NET SDK version: {VersionString}", latestVersionNumber); return new CLICheckResponse(1, "", $"Failed to parse .NET SDK version: {latestVersionNumber}"); } } @@ -176,6 +253,8 @@ private async ValueTask VerifyDotnetVersion() var normalizedPath = packagePath.Replace('\\', '/'); var sdkIndex = normalizedPath.IndexOf("/sdk/", StringComparison.OrdinalIgnoreCase); + _logger.LogDebug("Parsing service directory from path: {PackagePath}", packagePath); + if (sdkIndex >= 0) { var pathAfterSdk = normalizedPath.Substring(sdkIndex + 5); // Skip "/sdk/" @@ -183,8 +262,18 @@ private async ValueTask VerifyDotnetVersion() if (segments.Length > 0) { serviceDirectory = segments[0]; + _logger.LogDebug("Extracted service directory: {ServiceDirectory}", serviceDirectory); + } + else + { + _logger.LogDebug("No segments found after /sdk/ in path"); } } + else + { + _logger.LogDebug("Path does not contain /sdk/ segment"); + } + return serviceDirectory; } From 8ac04518f7880c19f1f57e6b2da4ec7c0d6dd508 Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Wed, 15 Oct 2025 09:59:59 -0700 Subject: [PATCH 04/16] fix tests --- .../DotNetLanguageSpecificChecksTests.cs | 83 ++++++++++++++----- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs index d9205e18b56..a528161dcb0 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs @@ -267,6 +267,11 @@ public async Task TestCheckGeneratedCodeAsyncSuccessReturnsSuccess() SetupSuccessfulDotNetVersionCheck(); SetupGitRepoDiscovery(); + // Create the expected script path and ensure the directory exists + var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "CodeChecks.ps1"); + Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); + File.WriteAllText(scriptPath, "# Mock PowerShell script"); + var processResult = new ProcessResult { ExitCode = 0 }; processResult.AppendStdout("All checks passed successfully!"); @@ -278,13 +283,21 @@ public async Task TestCheckGeneratedCodeAsyncSuccessReturnsSuccess() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); + try + { + var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); - Assert.Multiple(() => + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(0)); + Assert.That(result.CheckStatusDetails, Does.Contain("All checks passed successfully")); + }); + } + finally { - Assert.That(result.ExitCode, Is.EqualTo(0)); - Assert.That(result.CheckStatusDetails, Does.Contain("All checks passed successfully")); - }); + // Cleanup + try { File.Delete(scriptPath); Directory.Delete(Path.GetDirectoryName(scriptPath)!, true); } catch { } + } } [TestCase(true)] @@ -302,6 +315,12 @@ public async Task TestCheckGeneratedCodeAsyncSpellCheckFailureReturnsError(bool "Spell check failed"; SetupSuccessfulDotNetVersionCheck(); + SetupGitRepoDiscovery(); + + // Create the expected script path and ensure the directory exists + var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "CodeChecks.ps1"); + Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); + File.WriteAllText(scriptPath, "# Mock PowerShell script"); _processHelperMock .Setup(x => x.Run( @@ -312,12 +331,10 @@ public async Task TestCheckGeneratedCodeAsyncSpellCheckFailureReturnsError(bool .ReturnsAsync(() => { var processResult = new ProcessResult { ExitCode = 1 }; - processResult.AppendStderr(errorMessage); + processResult.AppendStdout(errorMessage); // Changed from AppendStderr to AppendStdout return processResult; }); - SetupGitRepoDiscovery(); - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); Assert.Multiple(() => @@ -373,6 +390,11 @@ public async Task TestCheckAotCompatAsyncSuccessReturnsSuccess() SetupSuccessfulDotNetVersionCheck(); SetupGitRepoDiscovery(); + // Create the expected script path and ensure the directory exists + var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); + Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); + File.WriteAllText(scriptPath, "# Mock PowerShell script"); + var processResult = new ProcessResult { ExitCode = 0 }; processResult.AppendStdout("AOT compatibility check passed!"); @@ -384,13 +406,21 @@ public async Task TestCheckAotCompatAsyncSuccessReturnsSuccess() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.CheckAotCompatAsync(_packagePath, CancellationToken.None); + try + { + var result = await _languageChecks.CheckAotCompatAsync(_packagePath, CancellationToken.None); - Assert.Multiple(() => + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(0)); + Assert.That(result.CheckStatusDetails, Does.Contain("AOT compatibility check passed")); + }); + } + finally { - Assert.That(result.ExitCode, Is.EqualTo(0)); - Assert.That(result.CheckStatusDetails, Does.Contain("AOT compatibility check passed")); - }); + // Cleanup + try { File.Delete(scriptPath); Directory.Delete(Path.GetDirectoryName(scriptPath)!, true); } catch { } + } } [Test] @@ -399,12 +429,17 @@ public async Task TestCheckAotCompatAsyncAotWarningsReturnsError() SetupSuccessfulDotNetVersionCheck(); SetupGitRepoDiscovery(); + // Create the expected script path and ensure the directory exists + var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); + Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); + File.WriteAllText(scriptPath, "# Mock PowerShell script"); + var errorMessage = "ILLink : Trim analysis warning IL2026: Azure.Storage.Blobs.BlobClient.Upload: " + "Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' " + "can break functionality when trimming application code."; var processResult = new ProcessResult { ExitCode = 1 }; - processResult.AppendStderr(errorMessage); + processResult.AppendStdout(errorMessage); // Changed from AppendStderr to AppendStdout _processHelperMock .Setup(x => x.Run( @@ -414,13 +449,21 @@ public async Task TestCheckAotCompatAsyncAotWarningsReturnsError() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.CheckAotCompatAsync(_packagePath, CancellationToken.None); + try + { + var result = await _languageChecks.CheckAotCompatAsync(_packagePath, CancellationToken.None); - Assert.Multiple(() => + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.CheckStatusDetails, Does.Contain("Trim analysis warning")); + Assert.That(result.CheckStatusDetails, Does.Contain("RequiresUnreferencedCodeAttribute")); + }); + } + finally { - Assert.That(result.ExitCode, Is.EqualTo(1)); - Assert.That(result.CheckStatusDetails, Does.Contain("Trim analysis warning")); - Assert.That(result.CheckStatusDetails, Does.Contain("RequiresUnreferencedCodeAttribute")); - }); + // Cleanup + try { File.Delete(scriptPath); Directory.Delete(Path.GetDirectoryName(scriptPath)!, true); } catch { } + } } } From 54d9c4dec10645334ae4cfb590da288a2da546fb Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Wed, 15 Oct 2025 10:03:12 -0700 Subject: [PATCH 05/16] fix merge --- .../Services/Languages/LanguageChecks.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs index 1ff1bbbd8f0..8dcaf261156 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs @@ -100,14 +100,6 @@ public interface ILanguageChecks /// Cancellation token /// Result of the generated code check Task CheckGeneratedCodeAsync(string packagePath, CancellationToken ct = default); - - /// - /// Gets the SDK package path for the given repository and package path. - /// - /// Repository root path - /// Package path - /// SDK package path - string GetSDKPackagePath(string repo, string packagePath); } /// From 31b1080a9e87ebbb73a7bba1ef87b5ae10d69148 Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Thu, 16 Oct 2025 10:27:30 -0700 Subject: [PATCH 06/16] clean up --- .../DotNetLanguageSpecificChecksTests.cs | 183 +----------------- .../Models/PackageCheckType.cs | 12 +- .../Languages/DotNetLanguageSpecificChecks.cs | 122 +++--------- .../Languages/ILanguageSpecificChecks.cs | 17 +- .../Services/Languages/LanguageChecks.cs | 55 ++---- .../Tools/Package/PackageCheckTool.cs | 15 +- 6 files changed, 68 insertions(+), 336 deletions(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs index a528161dcb0..0b9186f05dd 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs @@ -54,173 +54,6 @@ private void SetupGitRepoDiscovery() .Returns(_repoRoot); } - [Test] - public async Task TestPackCodeAsyncDotNetNotInstalledReturnsError() - { - var processResult = new ProcessResult { ExitCode = 1 }; - processResult.AppendStderr("dotnet command not found"); - - _processHelperMock - .Setup(x => x.Run( - It.Is(p => - p.Command == "cmd.exe" && - p.Args.Contains("/C") && - p.Args.Contains("dotnet") && - p.Args.Contains("--list-sdks")), - It.IsAny())) - .ReturnsAsync(processResult); - - var result = await _languageChecks.PackCodeAsync(_packagePath, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.ExitCode, Is.EqualTo(1)); - Assert.That(result.CheckStatusDetails, Does.Contain("dotnet --list-sdks failed")); - }); - - _processHelperMock.Verify( - x => x.Run( - It.Is(p => p.Args.Contains("pack")), - It.IsAny()), - Times.Never); - } - - [Test] - public async Task TestPackCodeAsyncOldDotNetVersionReturnsError() - { - var versionOutput = "8.0.100 [C:\\Program Files\\dotnet\\sdk]\n8.0.200 [C:\\Program Files\\dotnet\\sdk]"; - var processResult = new ProcessResult { ExitCode = 0 }; - processResult.AppendStdout(versionOutput); - - _processHelperMock - .Setup(x => x.Run( - It.Is(p => - p.Command == "cmd.exe" && - p.Args.Contains("/C") && - p.Args.Contains("dotnet") && - p.Args.Contains("--list-sdks")), - It.IsAny())) - .ReturnsAsync(processResult); - - var result = await _languageChecks.PackCodeAsync(_packagePath, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.ExitCode, Is.EqualTo(1)); - Assert.That(result.ResponseError, Does.Contain("below minimum requirement")); - Assert.That(result.ResponseError, Does.Contain(RequiredDotNetVersion)); - }); - } - - [Test] - public async Task TestPackCodeAsyncInvalidPackagePathReturnsError() - { - SetupSuccessfulDotNetVersionCheck(); - var invalidPath = "/tmp/not-in-sdk-folder"; - - var result = await _languageChecks.PackCodeAsync(invalidPath, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.ExitCode, Is.EqualTo(1)); - Assert.That(result.ResponseError, Does.Contain("Failed to determine service directory")); - }); - } - - [Test] - public async Task TestPackCodeAsyncSuccessReturnsSuccess() - { - SetupSuccessfulDotNetVersionCheck(); - - var processResult = new ProcessResult { ExitCode = 0 }; - processResult.AppendStdout( - "Microsoft (R) Build Engine version 17.10.0+1\n" + - "Build started 1/9/2025 10:30:00 AM.\n" + - " Determining projects to restore...\n" + - " All projects are up-to-date for restore.\n" + - " Azure.Storage.Blobs -> bin/Release/net8.0/Azure.Storage.Blobs.dll\n" + - " Successfully created package 'bin/Release/Azure.Storage.Blobs.1.0.0.nupkg'.\n" + - "Build succeeded.\n" + - " 0 Warning(s)\n" + - " 0 Error(s)" - ); - - _processHelperMock - .Setup(x => x.Run( - It.Is(p => - p.Command == "cmd.exe" && - p.Args.Contains("/C") && - p.Args.Contains("dotnet") && - p.Args.Contains("pack") && - p.Args.Any(a => a.Contains("service.proj"))), - It.IsAny())) - .ReturnsAsync(processResult); - - SetupGitRepoDiscovery(); - - var result = await _languageChecks.PackCodeAsync(_packagePath, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.ExitCode, Is.EqualTo(0)); - Assert.That(result.CheckStatusDetails, Does.Contain("Build succeeded")); - }); - - _processHelperMock.Verify( - x => x.Run( - It.Is(p => - p.Command == "cmd.exe" && - p.Args.Contains("/C") && - p.Args.Contains("dotnet") && - p.Args.Contains("pack") && - p.Args.Any(a => a.Contains("service.proj")) && - p.Args.Contains("-warnaserror") && - p.Args.Contains("/p:ValidateRunApiCompat=true") && - p.Args.Contains("/p:SDKType=client") && - p.Args.Contains("/p:ServiceDirectory=storage") && - p.Args.Contains("/p:IncludeTests=false") && - p.Args.Contains("/p:PublicSign=false") && - p.Args.Contains("/p:Configuration=Release") && - p.Args.Contains("/p:IncludePerf=false") && - p.Args.Contains("/p:IncludeStress=false") && - p.Args.Contains("/p:IncludeIntegrationTests=false") && - p.WorkingDirectory == _repoRoot && - p.Timeout == TimeSpan.FromMinutes(10)), - It.IsAny()), - Times.Once); - } - - [Test] - public async Task TestPackCodeAsyncBuildFailureReturnsError() - { - SetupSuccessfulDotNetVersionCheck(); - - _processHelperMock - .Setup(x => x.Run( - It.Is(p => - p.Command == "cmd.exe" && - p.Args.Contains("/C") && - p.Args.Contains("dotnet") && - p.Args.Contains("pack")), - It.IsAny())) - .ReturnsAsync(() => - { - var processResult = new ProcessResult { ExitCode = 1 }; - processResult.AppendStderr("error CS0246: The type or namespace name 'Azure' could not be found"); - return processResult; - }); - - SetupGitRepoDiscovery(); - - var result = await _languageChecks.PackCodeAsync(_packagePath, CancellationToken.None); - - Assert.Multiple(() => - { - Assert.That(result.ExitCode, Is.EqualTo(1)); - Assert.That(result.CheckStatusDetails, Does.Contain("could not be found")); - }); - } - [Test] public async Task TestCheckGeneratedCodeAsyncDotNetNotInstalledReturnsError() { @@ -237,7 +70,7 @@ public async Task TestCheckGeneratedCodeAsyncDotNetNotInstalledReturnsError() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -252,7 +85,7 @@ public async Task TestCheckGeneratedCodeAsyncInvalidPackagePathReturnsError() SetupSuccessfulDotNetVersionCheck(); var invalidPath = "/tmp/not-in-sdk-folder"; - var result = await _languageChecks.CheckGeneratedCodeAsync(invalidPath, CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCodeAsync(invalidPath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -285,7 +118,7 @@ public async Task TestCheckGeneratedCodeAsyncSuccessReturnsSuccess() try { - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -335,7 +168,7 @@ public async Task TestCheckGeneratedCodeAsyncSpellCheckFailureReturnsError(bool return processResult; }); - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -360,7 +193,7 @@ public async Task TestCheckAotCompatAsyncDotNetNotInstalledReturnsError() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.CheckAotCompatAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.CheckAotCompatAsync(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -375,7 +208,7 @@ public async Task TestCheckAotCompatAsyncInvalidPackagePathReturnsError() SetupSuccessfulDotNetVersionCheck(); var invalidPath = "/tmp/not-in-sdk-folder"; - var result = await _languageChecks.CheckAotCompatAsync(invalidPath, CancellationToken.None); + var result = await _languageChecks.CheckAotCompatAsync(invalidPath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -408,7 +241,7 @@ public async Task TestCheckAotCompatAsyncSuccessReturnsSuccess() try { - var result = await _languageChecks.CheckAotCompatAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.CheckAotCompatAsync(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -451,7 +284,7 @@ public async Task TestCheckAotCompatAsyncAotWarningsReturnsError() try { - var result = await _languageChecks.CheckAotCompatAsync(_packagePath, CancellationToken.None); + var result = await _languageChecks.CheckAotCompatAsync(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Models/PackageCheckType.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Models/PackageCheckType.cs index 0f5196bfd97..80df27fc372 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Models/PackageCheckType.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Models/PackageCheckType.cs @@ -43,6 +43,16 @@ public enum PackageCheckType /// /// Format code /// - Format + Format, + + /// + /// .NET validation for AOT compatibility. + /// + CheckAotCompat, + + /// + /// .NET validation for generated code. + /// + GeneratedCodeChecks } diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs index ed9c83a97ca..6368136197a 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs @@ -14,7 +14,7 @@ public class DotNetLanguageSpecificChecks : ILanguageSpecificChecks private readonly ILogger _logger; private const string DotNetCommand = "dotnet"; private const string PowerShellCommand = "pwsh"; - private const string RequiredDotNetVersion = "9.0.102"; + private const string RequiredDotNetVersion = "9.0.102"; // TODO - centralize this as part of env setup tool public DotNetLanguageSpecificChecks( IProcessHelper processHelper, @@ -26,16 +26,16 @@ public DotNetLanguageSpecificChecks( _logger = logger; } - public async Task PackCodeAsync(string packagePath, CancellationToken cancellationToken = default) + public async Task CheckGeneratedCodeAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) { try { - _logger.LogInformation("Starting code packing for .NET project at: {PackagePath}", packagePath); + _logger.LogInformation("Starting generated code checks for .NET project at: {PackagePath}", packagePath); var dotnetVersionValidation = await VerifyDotnetVersion(); if (dotnetVersionValidation.ExitCode != 0) { - _logger.LogError("Dotnet version validation failed"); + _logger.LogError("Dotnet version validation failed for generated code checks"); return dotnetVersionValidation; } @@ -46,54 +46,40 @@ public async Task PackCodeAsync(string packagePath, Cancellati return new CLICheckResponse(1, "", "Failed to determine service directory from the provided package path."); } - _logger.LogInformation("Determined service directory: {ServiceDirectory}", serviceDirectory); - var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); - _logger.LogInformation("Found repository root at: {RepoRoot}", repoRoot); - - var serviceProj = Path.Combine(repoRoot, "eng", "service.proj"); - _logger.LogInformation("Using service project file: {ServiceProj}", serviceProj); + var scriptPath = Path.Combine(repoRoot, "eng", "scripts", "CodeChecks.ps1"); - // Run dotnet pack with the same parameters as CI - var args = new List + if (!File.Exists(scriptPath)) { - "pack", - serviceProj, - "-warnaserror", - "/p:ValidateRunApiCompat=true", - "/p:SDKType=client", - $"/p:ServiceDirectory={serviceDirectory}", - "/p:IncludeTests=false", - "/p:PublicSign=false", - "/p:Configuration=Release", - "/p:IncludePerf=false", - "/p:IncludeStress=false", - "/p:IncludeIntegrationTests=false" - }; + _logger.LogError("Code checks script not found at: {ScriptPath}", scriptPath); + return new CLICheckResponse(1, "", $"Code checks script not found at: {scriptPath}"); + } + + var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-SpellCheckPublicApiSurface" }; + _logger.LogInformation("Executing command: {Command} {Arguments}", PowerShellCommand, string.Join(" ", args)); + + var timeout = TimeSpan.FromMinutes(6); + var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: repoRoot), ct); - _logger.LogInformation("Executing command: {Command} {Arguments}", DotNetCommand, string.Join(" ", args)); - var timeout = TimeSpan.FromMinutes(10); - var result = await _processHelper.Run(new ProcessOptions(DotNetCommand, args.ToArray(), timeout: timeout, workingDirectory: repoRoot), cancellationToken); - if (result.ExitCode == 0) { - _logger.LogInformation("Code packing completed successfully"); + _logger.LogInformation("Generated code checks completed successfully"); + return new CLICheckResponse(result.ExitCode, result.Output); } else { - _logger.LogWarning("Code packing failed with exit code {ExitCode}", result.ExitCode); + _logger.LogWarning("Generated code checks failed with exit code {ExitCode}", result.ExitCode); + return new CLICheckResponse(result.ExitCode, result.Output, "Generated code checks failed"); } - - return new CLICheckResponse(result); } catch (Exception ex) { - _logger.LogError(ex, "{MethodName} failed with an exception", nameof(PackCodeAsync)); - return new CLICheckResponse(1, "", $"{nameof(PackCodeAsync)} failed with an exception: {ex.Message}"); + _logger.LogError(ex, "{MethodName} failed with an exception", nameof(CheckGeneratedCodeAsync)); + return new CLICheckResponse(1, "", $"{nameof(CheckGeneratedCodeAsync)} failed with an exception: {ex.Message}"); } } - public async Task CheckAotCompatAsync(string packagePath, CancellationToken cancellationToken = default) + public async Task CheckAotCompatAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) { try { @@ -114,13 +100,9 @@ public async Task CheckAotCompatAsync(string packagePath, Canc return new CLICheckResponse(1, "", "Failed to determine service directory or package name from the provided package path."); } - _logger.LogInformation("Determined service directory: {ServiceDirectory}, package name: {PackageName}", serviceDirectory, packageName); - var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); - _logger.LogInformation("Found repository root at: {RepoRoot}", repoRoot); var scriptPath = Path.Combine(repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); - _logger.LogInformation("Using AOT compatibility script: {ScriptPath}", scriptPath); if (!File.Exists(scriptPath)) { @@ -132,7 +114,7 @@ public async Task CheckAotCompatAsync(string packagePath, Canc _logger.LogInformation("Executing command: {Command} {Arguments}", PowerShellCommand, string.Join(" ", args)); var timeout = TimeSpan.FromMinutes(6); - var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: repoRoot), cancellationToken); + var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: repoRoot), ct); if (result.ExitCode == 0) { @@ -152,64 +134,6 @@ public async Task CheckAotCompatAsync(string packagePath, Canc } } - public async Task CheckGeneratedCodeAsync(string packagePath, CancellationToken cancellationToken = default) - { - try - { - _logger.LogInformation("Starting generated code checks for .NET project at: {PackagePath}", packagePath); - - var dotnetVersionValidation = await VerifyDotnetVersion(); - if (dotnetVersionValidation.ExitCode != 0) - { - _logger.LogError("Dotnet version validation failed for generated code checks"); - return dotnetVersionValidation; - } - - var serviceDirectory = GetServiceDirectoryFromPath(packagePath); - if (serviceDirectory == null) - { - _logger.LogError("Failed to determine service directory from package path: {PackagePath}", packagePath); - return new CLICheckResponse(1, "", "Failed to determine service directory from the provided package path."); - } - - _logger.LogInformation("Determined service directory: {ServiceDirectory}", serviceDirectory); - - var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); - _logger.LogInformation("Found repository root at: {RepoRoot}", repoRoot); - - var scriptPath = Path.Combine(repoRoot, "eng", "scripts", "CodeChecks.ps1"); - _logger.LogInformation("Using code checks script: {ScriptPath}", scriptPath); - - if (!File.Exists(scriptPath)) - { - _logger.LogError("Code checks script not found at: {ScriptPath}", scriptPath); - return new CLICheckResponse(1, "", $"Code checks script not found at: {scriptPath}"); - } - - var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-SpellCheckPublicApiSurface" }; - _logger.LogInformation("Executing command: {Command} {Arguments}", PowerShellCommand, string.Join(" ", args)); - - var timeout = TimeSpan.FromMinutes(6); - var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: repoRoot), cancellationToken); - - if (result.ExitCode == 0) - { - _logger.LogInformation("Generated code checks completed successfully"); - return new CLICheckResponse(result.ExitCode, result.Output); - } - else - { - _logger.LogWarning("Generated code checks failed with exit code {ExitCode}", result.ExitCode); - return new CLICheckResponse(result.ExitCode, result.Output, "Generated code checks failed"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "{MethodName} failed with an exception", nameof(CheckGeneratedCodeAsync)); - return new CLICheckResponse(1, "", $"{nameof(CheckGeneratedCodeAsync)} failed with an exception: {ex.Message}"); - } - } - private async ValueTask VerifyDotnetVersion() { _logger.LogDebug("Verifying .NET SDK version"); diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs index b1845f29628..f8a93fe6264 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs @@ -55,24 +55,13 @@ Task FormatCodeAsync(string packagePath, bool fixCheckErrors = return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); } - /// - /// Packs code in the specific package using language-specific tools. - /// - /// Path to the package directory - /// Cancellation token - /// Result of the code packing operation - Task PackCodeAsync(string packagePath, CancellationToken cancellationToken = default) - { - return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); - } - /// /// Checks AOT compatibility for the specific package using language-specific tools. /// /// Path to the package directory /// Cancellation token /// Result of the AOT compatibility check - Task CheckAotCompatAsync(string packagePath, CancellationToken cancellationToken = default) + Task CheckAotCompatAsync(string packagePath, bool fixCheckErrors = false, CancellationToken cancellationToken = default) { return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); } @@ -83,7 +72,7 @@ Task CheckAotCompatAsync(string packagePath, CancellationToken /// Path to the package directory /// Cancellation token /// Result of the generated code check - Task CheckGeneratedCodeAsync(string packagePath, CancellationToken cancellationToken = default) + Task CheckGeneratedCodeAsync(string packagePath, bool fixCheckErrors = false, CancellationToken cancellationToken = default) { return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); } @@ -98,4 +87,4 @@ Task GetSDKPackageName(string repo, string packagePath, CancellationToke // Default implementation: use the directory name as the package path return Task.FromResult(Path.GetFileName(packagePath)); } -} \ No newline at end of file +} diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs index 8dcaf261156..c42ba3edffb 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs @@ -1,11 +1,13 @@ -using Azure.Sdk.Tools.Cli.Models; -using Azure.Sdk.Tools.Cli.Helpers; +using System.ComponentModel; +using System.Threading; using Azure.Sdk.Tools.Cli.Configuration; -using Azure.Sdk.Tools.Cli.Prompts; -using Azure.Sdk.Tools.Cli.Prompts.Templates; +using Azure.Sdk.Tools.Cli.Helpers; using Azure.Sdk.Tools.Cli.Microagents; using Azure.Sdk.Tools.Cli.Microagents.Tools; -using System.ComponentModel; +using Azure.Sdk.Tools.Cli.Models; +using Azure.Sdk.Tools.Cli.Prompts; +using Azure.Sdk.Tools.Cli.Prompts.Templates; +using ModelContextProtocol.Protocol; namespace Azure.Sdk.Tools.Cli.Services; @@ -77,21 +79,13 @@ public interface ILanguageChecks /// Result of the code formatting operation Task FormatCodeAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default); - /// - /// Packs code in the specific package. - /// - /// Path to the package directory - /// Cancellation token - /// Result of the code packing operation - Task PackCodeAsync(string packagePath, CancellationToken ct = default); - /// /// Checks AOT compatibility for the specific package. /// /// Path to the package directory /// Cancellation token /// Result of the AOT compatibility check - Task CheckAotCompatAsync(string packagePath, CancellationToken ct = default); + Task CheckAotCompatAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default); /// /// Checks generated code for the specific package. @@ -99,7 +93,7 @@ public interface ILanguageChecks /// Path to the package directory /// Cancellation token /// Result of the generated code check - Task CheckGeneratedCodeAsync(string packagePath, CancellationToken ct = default); + Task CheckGeneratedCodeAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default); } /// @@ -113,6 +107,7 @@ public class LanguageChecks : ILanguageChecks private readonly ILogger _logger; private readonly ILanguageSpecificResolver _languageSpecificChecks; private readonly IMicroagentHostService _microagentHostService; + private const string PowerShellCommand = "pwsh"; public LanguageChecks(IProcessHelper processHelper, INpxHelper npxHelper, IGitHelper gitHelper, ILogger logger, ILanguageSpecificResolver languageSpecificChecks, IMicroagentHostService microagentHostService) { @@ -178,6 +173,11 @@ public virtual async Task CheckSpellingAsync(string packagePat return await CheckSpellingCommonAsync(packagePath, fixCheckErrors, ct); } + public Task ValidateFilePathsAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) + { + throw new NotImplementedException(); + } + public virtual async Task UpdateSnippetsAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) { var languageSpecificCheck = await _languageSpecificChecks.Resolve(packagePath); @@ -229,24 +229,7 @@ public virtual async Task FormatCodeAsync(string packagePath, return await languageSpecificCheck.FormatCodeAsync(packagePath, fixCheckErrors, ct); } - public virtual async Task PackCodeAsync(string packagePath, CancellationToken ct = default) - { - var languageSpecificCheck = await _languageSpecificChecks.Resolve(packagePath); - - if (languageSpecificCheck == null) - { - _logger.LogError("No language-specific check handler found for package at {PackagePath}. Supported languages may not include this package type.", packagePath); - return new CLICheckResponse( - exitCode: 1, - checkStatusDetails: $"No language-specific check handler found for package at {packagePath}. Supported languages may not include this package type.", - error: "Unsupported package type" - ); - } - - return await languageSpecificCheck.PackCodeAsync(packagePath, ct); - } - - public virtual async Task CheckAotCompatAsync(string packagePath, CancellationToken ct = default) + public virtual async Task CheckAotCompatAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) { var languageSpecificCheck = await _languageSpecificChecks.Resolve(packagePath); @@ -260,10 +243,10 @@ public virtual async Task CheckAotCompatAsync(string packagePa ); } - return await languageSpecificCheck.CheckAotCompatAsync(packagePath, ct); + return await languageSpecificCheck.CheckAotCompatAsync(packagePath, fixCheckErrors, ct); } - public virtual async Task CheckGeneratedCodeAsync(string packagePath, CancellationToken ct = default) + public virtual async Task CheckGeneratedCodeAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) { var languageSpecificCheck = await _languageSpecificChecks.Resolve(packagePath); @@ -277,7 +260,7 @@ public virtual async Task CheckGeneratedCodeAsync(string packa ); } - return await languageSpecificCheck.CheckGeneratedCodeAsync(packagePath, ct); + return await languageSpecificCheck.CheckGeneratedCodeAsync(packagePath, fixCheckErrors, ct); } /// diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs index 37f6da0843c..886724454bf 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs @@ -100,6 +100,8 @@ public async Task RunPackageCheck(string packagePath, PackageC PackageCheckType.Snippets => await RunSnippetUpdate(packagePath, fixCheckErrors, ct), PackageCheckType.Linting => await RunLintCode(packagePath, fixCheckErrors, ct), PackageCheckType.Format => await RunFormatCode(packagePath, fixCheckErrors, ct), + PackageCheckType.CheckAotCompat => await languageChecks.CheckAotCompatAsync(packagePath, fixCheckErrors, ct), + PackageCheckType.GeneratedCodeChecks => await languageChecks.CheckGeneratedCodeAsync(packagePath, fixCheckErrors, ct), _ => throw new ArgumentOutOfRangeException( nameof(checkType), checkType, @@ -184,17 +186,8 @@ private async Task RunAllChecks(string packagePath, bool fixCh failedChecks.Add("Format"); } - // Run code packing - var packCodeResult = await languageChecks.PackCodeAsync(packagePath, ct); - results.Add(packCodeResult); - if (packCodeResult.ExitCode != 0) - { - overallSuccess = false; - failedChecks.Add("Pack"); - } - // Run AOT compatibility check - var aotCompatResult = await languageChecks.CheckAotCompatAsync(packagePath, ct); + var aotCompatResult = await languageChecks.CheckAotCompatAsync(packagePath, fixCheckErrors, ct); results.Add(aotCompatResult); if (aotCompatResult.ExitCode != 0) { @@ -203,7 +196,7 @@ private async Task RunAllChecks(string packagePath, bool fixCh } // Run generated code check - var generatedCodeResult = await languageChecks.CheckGeneratedCodeAsync(packagePath, ct); + var generatedCodeResult = await languageChecks.CheckGeneratedCodeAsync(packagePath, fixCheckErrors, ct); results.Add(generatedCodeResult); if (generatedCodeResult.ExitCode != 0) { From faf614adc6289dc0a9891ae3bebfb293025abc77 Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Thu, 16 Oct 2025 11:11:27 -0700 Subject: [PATCH 07/16] fix linux/mac tests --- .../DotNetLanguageSpecificChecksTests.cs | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs index 0b9186f05dd..8bad471a352 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs @@ -38,11 +38,7 @@ private void SetupSuccessfulDotNetVersionCheck() _processHelperMock .Setup(x => x.Run( - It.Is(p => - p.Command == "cmd.exe" && - p.Args.Contains("/C") && - p.Args.Contains("dotnet") && - p.Args.Contains("--list-sdks")), + It.Is(p => IsDotNetListSdksCommand(p)), It.IsAny())) .ReturnsAsync(processResult); } @@ -62,11 +58,7 @@ public async Task TestCheckGeneratedCodeAsyncDotNetNotInstalledReturnsError() _processHelperMock .Setup(x => x.Run( - It.Is(p => - p.Command == "cmd.exe" && - p.Args.Contains("/C") && - p.Args.Contains("dotnet") && - p.Args.Contains("--list-sdks")), + It.Is(p => IsDotNetListSdksCommand(p)), It.IsAny())) .ReturnsAsync(processResult); @@ -111,7 +103,7 @@ public async Task TestCheckGeneratedCodeAsyncSuccessReturnsSuccess() _processHelperMock .Setup(x => x.Run( It.Is(p => - p.Command == "pwsh" && + IsPowerShellCommand(p) && p.Args.Any(a => a.Contains("CodeChecks.ps1"))), It.IsAny())) .ReturnsAsync(processResult); @@ -158,7 +150,7 @@ public async Task TestCheckGeneratedCodeAsyncSpellCheckFailureReturnsError(bool _processHelperMock .Setup(x => x.Run( It.Is(p => - p.Command == "pwsh" && + IsPowerShellCommand(p) && p.Args.Any(a => a.Contains("CodeChecks.ps1"))), It.IsAny())) .ReturnsAsync(() => @@ -185,11 +177,7 @@ public async Task TestCheckAotCompatAsyncDotNetNotInstalledReturnsError() _processHelperMock .Setup(x => x.Run( - It.Is(p => - p.Command == "cmd.exe" && - p.Args.Contains("/C") && - p.Args.Contains("dotnet") && - p.Args.Contains("--list-sdks")), + It.Is(p => IsDotNetListSdksCommand(p)), It.IsAny())) .ReturnsAsync(processResult); @@ -234,7 +222,7 @@ public async Task TestCheckAotCompatAsyncSuccessReturnsSuccess() _processHelperMock .Setup(x => x.Run( It.Is(p => - p.Command == "pwsh" && + IsPowerShellCommand(p) && p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), It.IsAny())) .ReturnsAsync(processResult); @@ -277,7 +265,7 @@ public async Task TestCheckAotCompatAsyncAotWarningsReturnsError() _processHelperMock .Setup(x => x.Run( It.Is(p => - p.Command == "pwsh" && + IsPowerShellCommand(p) && p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), It.IsAny())) .ReturnsAsync(processResult); @@ -299,4 +287,23 @@ public async Task TestCheckAotCompatAsyncAotWarningsReturnsError() try { File.Delete(scriptPath); Directory.Delete(Path.GetDirectoryName(scriptPath)!, true); } catch { } } } + + #region Helper Methods for Cross-Platform Command Validation + + /// + /// Checks if the ProcessOptions represents a dotnet --list-sdks command. + /// Handles both Unix (dotnet --list-sdks) and Windows (cmd.exe /C dotnet --list-sdks) patterns. + /// + private static bool IsDotNetListSdksCommand(ProcessOptions options) => + (options.Command == "dotnet" && options.Args.Contains("--list-sdks")) || + (options.Command == "cmd.exe" && options.Args.Contains("dotnet") && options.Args.Contains("--list-sdks")); + + /// + /// Checks if the ProcessOptions represents a PowerShell command. + /// Handles both Unix (pwsh) and Windows (pwsh) patterns. + /// + private static bool IsPowerShellCommand(ProcessOptions options) => + options.Command == "pwsh" || options.Command == "powershell"; + + #endregion } From 20a26f554a6f886197f84007b941d6aa5aa462df Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Thu, 16 Oct 2025 14:21:27 -0700 Subject: [PATCH 08/16] add aot opt out logic --- .../DotNetLanguageSpecificChecksTests.cs | 199 ++++++++++++++++++ .../Languages/DotNetLanguageSpecificChecks.cs | 50 +++++ 2 files changed, 249 insertions(+) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs index 8bad471a352..921cba259fd 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs @@ -288,6 +288,205 @@ public async Task TestCheckAotCompatAsyncAotWarningsReturnsError() } } + [Test] + public async Task TestCheckAotCompatAsyncWithOptOutReturnsSkipped() + { + SetupSuccessfulDotNetVersionCheck(); + SetupGitRepoDiscovery(); + + // Create a temporary directory structure for the test + var testPackagePath = Path.Combine(_repoRoot, "sdk", "testservice", "Azure.TestService"); + Directory.CreateDirectory(testPackagePath); + + // Create a .csproj file with AotCompatOptOut set to true + var csprojPath = Path.Combine(testPackagePath, "Azure.TestService.csproj"); + var csprojContent = @" + + net6.0 + true + +"; + File.WriteAllText(csprojPath, csprojContent); + + try + { + var result = await _languageChecks.CheckAotCompatAsync(testPackagePath, ct: CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(0)); + Assert.That(result.CheckStatusDetails, Does.Contain("AOT compatibility check skipped")); + Assert.That(result.CheckStatusDetails, Does.Contain("AotCompatOptOut is set to true")); + }); + } + finally + { + // Cleanup + try { Directory.Delete(testPackagePath, true); } catch { } + } + } + + [Test] + public async Task TestCheckAotCompatAsyncWithoutOptOutRunsCheck() + { + SetupSuccessfulDotNetVersionCheck(); + SetupGitRepoDiscovery(); + + // Create a temporary directory structure for the test + var testPackagePath = Path.Combine(_repoRoot, "sdk", "testservice", "Azure.TestService"); + Directory.CreateDirectory(testPackagePath); + + // Create a .csproj file without AotCompatOptOut (or set to false) + var csprojPath = Path.Combine(testPackagePath, "Azure.TestService.csproj"); + var csprojContent = @" + + net6.0 + +"; + File.WriteAllText(csprojPath, csprojContent); + + // Create the expected script path and ensure the directory exists + var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); + Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); + File.WriteAllText(scriptPath, "# Mock PowerShell script"); + + var processResult = new ProcessResult { ExitCode = 0 }; + processResult.AppendStdout("AOT compatibility check passed!"); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + IsPowerShellCommand(p) && + p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), + It.IsAny())) + .ReturnsAsync(processResult); + + try + { + var result = await _languageChecks.CheckAotCompatAsync(testPackagePath, ct: CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(0)); + Assert.That(result.CheckStatusDetails, Does.Contain("AOT compatibility check passed")); + Assert.That(result.CheckStatusDetails, Does.Not.Contain("skipped")); + }); + + // Verify that the PowerShell script was actually called + _processHelperMock.Verify(x => x.Run( + It.Is(p => + IsPowerShellCommand(p) && + p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), + It.IsAny()), Times.Once); + } + finally + { + // Cleanup + try + { + Directory.Delete(testPackagePath, true); + File.Delete(scriptPath); + Directory.Delete(Path.GetDirectoryName(scriptPath)!, true); + } + catch { } + } + } + + [Test] + public async Task TestCheckAotCompatAsyncOptOutCaseInsensitive() + { + SetupSuccessfulDotNetVersionCheck(); + SetupGitRepoDiscovery(); + + // Create a temporary directory structure for the test + var testPackagePath = Path.Combine(_repoRoot, "sdk", "testservice", "Azure.TestService"); + Directory.CreateDirectory(testPackagePath); + + // Create a .csproj file with AotCompatOptOut in different case combinations + var csprojPath = Path.Combine(testPackagePath, "Azure.TestService.csproj"); + var csprojContent = @" + + net6.0 + TRUE + +"; + File.WriteAllText(csprojPath, csprojContent); + + try + { + var result = await _languageChecks.CheckAotCompatAsync(testPackagePath, ct: CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(0)); + Assert.That(result.CheckStatusDetails, Does.Contain("AOT compatibility check skipped")); + Assert.That(result.CheckStatusDetails, Does.Contain("AotCompatOptOut is set to true")); + }); + } + finally + { + // Cleanup + try { Directory.Delete(testPackagePath, true); } catch { } + } + } + + [Test] + public async Task TestCheckAotCompatAsyncNoCsprojFileRunsCheck() + { + SetupSuccessfulDotNetVersionCheck(); + SetupGitRepoDiscovery(); + + // Create a temporary directory structure for the test without any .csproj file + var testPackagePath = Path.Combine(_repoRoot, "sdk", "testservice", "Azure.TestService"); + Directory.CreateDirectory(testPackagePath); + + // Create the expected script path and ensure the directory exists + var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); + Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); + File.WriteAllText(scriptPath, "# Mock PowerShell script"); + + var processResult = new ProcessResult { ExitCode = 0 }; + processResult.AppendStdout("AOT compatibility check passed!"); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => + IsPowerShellCommand(p) && + p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), + It.IsAny())) + .ReturnsAsync(processResult); + + try + { + var result = await _languageChecks.CheckAotCompatAsync(testPackagePath, ct: CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(0)); + Assert.That(result.CheckStatusDetails, Does.Contain("AOT compatibility check passed")); + Assert.That(result.CheckStatusDetails, Does.Not.Contain("skipped")); + }); + + // Verify that the PowerShell script was actually called since no opt-out was found + _processHelperMock.Verify(x => x.Run( + It.Is(p => + IsPowerShellCommand(p) && + p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), + It.IsAny()), Times.Once); + } + finally + { + // Cleanup + try + { + Directory.Delete(testPackagePath, true); + File.Delete(scriptPath); + Directory.Delete(Path.GetDirectoryName(scriptPath)!, true); + } + catch { } + } + } + #region Helper Methods for Cross-Platform Command Validation /// diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs index 6368136197a..13784839da3 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs @@ -100,6 +100,14 @@ public async Task CheckAotCompatAsync(string packagePath, bool return new CLICheckResponse(1, "", "Failed to determine service directory or package name from the provided package path."); } + // Check if AOT compatibility is opted out in the project file + var isAotOptedOut = CheckAotCompatOptOut(packagePath, packageName); + if (isAotOptedOut) + { + _logger.LogInformation("AOT compatibility check skipped - AotCompatOptOut is set to true in project file"); + return new CLICheckResponse(0, "AOT compatibility check skipped - AotCompatOptOut is set to true in project file"); + } + var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); var scriptPath = Path.Combine(repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); @@ -218,4 +226,46 @@ private async ValueTask VerifyDotnetVersion() } return packageName; } + + private bool CheckAotCompatOptOut(string packagePath, string packageName) + { + try + { + // Look for .csproj files in the package directory + var csprojFiles = Directory.GetFiles(packagePath, "*.csproj", SearchOption.AllDirectories); + + // Try to find the main project file first (matching package name) + var mainCsprojFile = csprojFiles.FirstOrDefault(f => + Path.GetFileNameWithoutExtension(f).Equals(packageName, StringComparison.OrdinalIgnoreCase)); + + // If no matching file found, use the first .csproj file + var csprojFile = mainCsprojFile ?? csprojFiles.FirstOrDefault(); + + if (csprojFile == null) + { + _logger.LogDebug("No .csproj file found in package path: {PackagePath}", packagePath); + return false; + } + + _logger.LogDebug("Checking AOT opt-out in project file: {CsprojFile}", csprojFile); + + var projectContent = File.ReadAllText(csprojFile); + + // Check for true (case-insensitive) + var hasAotOptOut = projectContent.Contains("true", StringComparison.OrdinalIgnoreCase); + + if (hasAotOptOut) + { + _logger.LogInformation("Found AotCompatOptOut=true in project file: {CsprojFile}", csprojFile); + return true; + } + + return false; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to check AotCompatOptOut in project file for package: {PackageName}", packageName); + return false; + } + } } From 53e45cd59fc01bbd1f4117ed0b2c2027aa0e992e Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Thu, 16 Oct 2025 14:30:49 -0700 Subject: [PATCH 09/16] fix AOT checks issue --- .../Services/Languages/DotNetLanguageSpecificChecks.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs index 13784839da3..11dc82b7000 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs @@ -109,8 +109,8 @@ public async Task CheckAotCompatAsync(string packagePath, bool } var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); - var scriptPath = Path.Combine(repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); + var workingDirectory = Path.Combine(repoRoot, "eng", "scripts", "compatibility"); if (!File.Exists(scriptPath)) { @@ -122,7 +122,7 @@ public async Task CheckAotCompatAsync(string packagePath, bool _logger.LogInformation("Executing command: {Command} {Arguments}", PowerShellCommand, string.Join(" ", args)); var timeout = TimeSpan.FromMinutes(6); - var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: repoRoot), ct); + var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: workingDirectory), ct); if (result.ExitCode == 0) { From efb7a9995b2186586133c5d7d693d0751d3cb0b4 Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Thu, 16 Oct 2025 14:50:32 -0700 Subject: [PATCH 10/16] clean up some extra logging --- .../Languages/DotNetLanguageSpecificChecks.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs index 11dc82b7000..c64cdedffd3 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs @@ -56,7 +56,6 @@ public async Task CheckGeneratedCodeAsync(string packagePath, } var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-SpellCheckPublicApiSurface" }; - _logger.LogInformation("Executing command: {Command} {Arguments}", PowerShellCommand, string.Join(" ", args)); var timeout = TimeSpan.FromMinutes(6); var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: repoRoot), ct); @@ -100,7 +99,6 @@ public async Task CheckAotCompatAsync(string packagePath, bool return new CLICheckResponse(1, "", "Failed to determine service directory or package name from the provided package path."); } - // Check if AOT compatibility is opted out in the project file var isAotOptedOut = CheckAotCompatOptOut(packagePath, packageName); if (isAotOptedOut) { @@ -119,8 +117,6 @@ public async Task CheckAotCompatAsync(string packagePath, bool } var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-PackageName", packageName }; - _logger.LogInformation("Executing command: {Command} {Arguments}", PowerShellCommand, string.Join(" ", args)); - var timeout = TimeSpan.FromMinutes(6); var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: workingDirectory), ct); @@ -144,8 +140,6 @@ public async Task CheckAotCompatAsync(string packagePath, bool private async ValueTask VerifyDotnetVersion() { - _logger.LogDebug("Verifying .NET SDK version"); - var dotnetSDKCheck = await _processHelper.Run(new ProcessOptions(DotNetCommand, ["--list-sdks"]), CancellationToken.None); if (dotnetSDKCheck.ExitCode != 0) { @@ -156,8 +150,6 @@ private async ValueTask VerifyDotnetVersion() var dotnetVersions = dotnetSDKCheck.Output.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); var latestVersionNumber = dotnetVersions[dotnetVersions.Length - 1].Split('[')[0].Trim(); - _logger.LogInformation("Found .NET SDK version: {LatestVersion}", latestVersionNumber); - if (Version.TryParse(latestVersionNumber, out var installedVersion) && Version.TryParse(RequiredDotNetVersion, out var minimumVersion)) { @@ -185,8 +177,6 @@ private async ValueTask VerifyDotnetVersion() var normalizedPath = packagePath.Replace('\\', '/'); var sdkIndex = normalizedPath.IndexOf("/sdk/", StringComparison.OrdinalIgnoreCase); - _logger.LogDebug("Parsing service directory from path: {PackagePath}", packagePath); - if (sdkIndex >= 0) { var pathAfterSdk = normalizedPath.Substring(sdkIndex + 5); // Skip "/sdk/" @@ -231,14 +221,9 @@ private bool CheckAotCompatOptOut(string packagePath, string packageName) { try { - // Look for .csproj files in the package directory var csprojFiles = Directory.GetFiles(packagePath, "*.csproj", SearchOption.AllDirectories); - - // Try to find the main project file first (matching package name) var mainCsprojFile = csprojFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).Equals(packageName, StringComparison.OrdinalIgnoreCase)); - - // If no matching file found, use the first .csproj file var csprojFile = mainCsprojFile ?? csprojFiles.FirstOrDefault(); if (csprojFile == null) @@ -247,11 +232,8 @@ private bool CheckAotCompatOptOut(string packagePath, string packageName) return false; } - _logger.LogDebug("Checking AOT opt-out in project file: {CsprojFile}", csprojFile); - var projectContent = File.ReadAllText(csprojFile); - // Check for true (case-insensitive) var hasAotOptOut = projectContent.Contains("true", StringComparison.OrdinalIgnoreCase); if (hasAotOptOut) From e5d85622eed2a6d6260586c585681cacf8f435d6 Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Thu, 16 Oct 2025 17:11:22 -0700 Subject: [PATCH 11/16] tweaks --- .../DotNetLanguageSpecificChecksTests.cs | 2 +- .../Services/Languages/LanguageChecks.cs | 5 ----- .../Tools/Package/PackageCheckTool.cs | 20 +++++++++++++++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs index 921cba259fd..80f62cf3cf9 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs @@ -27,7 +27,7 @@ public void SetUp() NullLogger.Instance); _repoRoot = Path.Combine(Path.GetTempPath(), "azure-sdk-for-net"); - _packagePath = Path.Combine(_repoRoot, "sdk", "storage", "Azure.Storage.Blobs"); + _packagePath = Path.Combine(_repoRoot, "sdk", "healthdataaiservices", "Azure.Health.Deidentification"); } private void SetupSuccessfulDotNetVersionCheck() diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs index c42ba3edffb..47807fa18a7 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs @@ -173,11 +173,6 @@ public virtual async Task CheckSpellingAsync(string packagePat return await CheckSpellingCommonAsync(packagePath, fixCheckErrors, ct); } - public Task ValidateFilePathsAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) - { - throw new NotImplementedException(); - } - public virtual async Task UpdateSnippetsAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) { var languageSpecificCheck = await _languageSpecificChecks.Resolve(packagePath); diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs index 886724454bf..124d00dadf7 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs @@ -100,8 +100,8 @@ public async Task RunPackageCheck(string packagePath, PackageC PackageCheckType.Snippets => await RunSnippetUpdate(packagePath, fixCheckErrors, ct), PackageCheckType.Linting => await RunLintCode(packagePath, fixCheckErrors, ct), PackageCheckType.Format => await RunFormatCode(packagePath, fixCheckErrors, ct), - PackageCheckType.CheckAotCompat => await languageChecks.CheckAotCompatAsync(packagePath, fixCheckErrors, ct), - PackageCheckType.GeneratedCodeChecks => await languageChecks.CheckGeneratedCodeAsync(packagePath, fixCheckErrors, ct), + PackageCheckType.CheckAotCompat => await RunCheckAotCompat(packagePath, fixCheckErrors, ct), + PackageCheckType.GeneratedCodeChecks => await RunCheckGeneratedCode(packagePath, fixCheckErrors, ct), _ => throw new ArgumentOutOfRangeException( nameof(checkType), checkType, @@ -345,5 +345,21 @@ private async Task RunFormatCode(string packagePath, bool fixC var result = await languageChecks.FormatCodeAsync(packagePath, fixCheckErrors, ct); return result; } + + private async Task RunCheckGeneratedCode(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) + { + logger.LogInformation("Running generated code checks"); + + var result = await languageChecks.CheckGeneratedCodeAsync(packagePath, fixCheckErrors, ct); + return result; + } + + private async Task RunCheckAotCompat(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) + { + logger.LogInformation("Running AOT compatibility checks"); + + var result = await languageChecks.CheckAotCompatAsync(packagePath, fixCheckErrors, ct); + return result; + } } } From 6e286386235eb625162d84f7e0d1374980cb52fa Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Thu, 16 Oct 2025 17:57:25 -0700 Subject: [PATCH 12/16] cleaning up --- .../DotNetLanguageSpecificChecksTests.cs | 26 ++----------------- .../Languages/DotNetLanguageSpecificChecks.cs | 22 +++++++--------- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs index 80f62cf3cf9..8fe81ea5077 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs @@ -92,7 +92,6 @@ public async Task TestCheckGeneratedCodeAsyncSuccessReturnsSuccess() SetupSuccessfulDotNetVersionCheck(); SetupGitRepoDiscovery(); - // Create the expected script path and ensure the directory exists var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "CodeChecks.ps1"); Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); File.WriteAllText(scriptPath, "# Mock PowerShell script"); @@ -120,7 +119,6 @@ public async Task TestCheckGeneratedCodeAsyncSuccessReturnsSuccess() } finally { - // Cleanup try { File.Delete(scriptPath); Directory.Delete(Path.GetDirectoryName(scriptPath)!, true); } catch { } } } @@ -142,7 +140,6 @@ public async Task TestCheckGeneratedCodeAsyncSpellCheckFailureReturnsError(bool SetupSuccessfulDotNetVersionCheck(); SetupGitRepoDiscovery(); - // Create the expected script path and ensure the directory exists var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "CodeChecks.ps1"); Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); File.WriteAllText(scriptPath, "# Mock PowerShell script"); @@ -156,7 +153,7 @@ public async Task TestCheckGeneratedCodeAsyncSpellCheckFailureReturnsError(bool .ReturnsAsync(() => { var processResult = new ProcessResult { ExitCode = 1 }; - processResult.AppendStdout(errorMessage); // Changed from AppendStderr to AppendStdout + processResult.AppendStdout(errorMessage); return processResult; }); @@ -211,7 +208,6 @@ public async Task TestCheckAotCompatAsyncSuccessReturnsSuccess() SetupSuccessfulDotNetVersionCheck(); SetupGitRepoDiscovery(); - // Create the expected script path and ensure the directory exists var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); File.WriteAllText(scriptPath, "# Mock PowerShell script"); @@ -239,7 +235,6 @@ public async Task TestCheckAotCompatAsyncSuccessReturnsSuccess() } finally { - // Cleanup try { File.Delete(scriptPath); Directory.Delete(Path.GetDirectoryName(scriptPath)!, true); } catch { } } } @@ -250,7 +245,6 @@ public async Task TestCheckAotCompatAsyncAotWarningsReturnsError() SetupSuccessfulDotNetVersionCheck(); SetupGitRepoDiscovery(); - // Create the expected script path and ensure the directory exists var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); File.WriteAllText(scriptPath, "# Mock PowerShell script"); @@ -260,7 +254,7 @@ public async Task TestCheckAotCompatAsyncAotWarningsReturnsError() "can break functionality when trimming application code."; var processResult = new ProcessResult { ExitCode = 1 }; - processResult.AppendStdout(errorMessage); // Changed from AppendStderr to AppendStdout + processResult.AppendStdout(errorMessage); _processHelperMock .Setup(x => x.Run( @@ -283,7 +277,6 @@ public async Task TestCheckAotCompatAsyncAotWarningsReturnsError() } finally { - // Cleanup try { File.Delete(scriptPath); Directory.Delete(Path.GetDirectoryName(scriptPath)!, true); } catch { } } } @@ -294,11 +287,9 @@ public async Task TestCheckAotCompatAsyncWithOptOutReturnsSkipped() SetupSuccessfulDotNetVersionCheck(); SetupGitRepoDiscovery(); - // Create a temporary directory structure for the test var testPackagePath = Path.Combine(_repoRoot, "sdk", "testservice", "Azure.TestService"); Directory.CreateDirectory(testPackagePath); - // Create a .csproj file with AotCompatOptOut set to true var csprojPath = Path.Combine(testPackagePath, "Azure.TestService.csproj"); var csprojContent = @" @@ -321,7 +312,6 @@ public async Task TestCheckAotCompatAsyncWithOptOutReturnsSkipped() } finally { - // Cleanup try { Directory.Delete(testPackagePath, true); } catch { } } } @@ -332,11 +322,9 @@ public async Task TestCheckAotCompatAsyncWithoutOptOutRunsCheck() SetupSuccessfulDotNetVersionCheck(); SetupGitRepoDiscovery(); - // Create a temporary directory structure for the test var testPackagePath = Path.Combine(_repoRoot, "sdk", "testservice", "Azure.TestService"); Directory.CreateDirectory(testPackagePath); - // Create a .csproj file without AotCompatOptOut (or set to false) var csprojPath = Path.Combine(testPackagePath, "Azure.TestService.csproj"); var csprojContent = @" @@ -345,7 +333,6 @@ public async Task TestCheckAotCompatAsyncWithoutOptOutRunsCheck() "; File.WriteAllText(csprojPath, csprojContent); - // Create the expected script path and ensure the directory exists var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); File.WriteAllText(scriptPath, "# Mock PowerShell script"); @@ -372,7 +359,6 @@ public async Task TestCheckAotCompatAsyncWithoutOptOutRunsCheck() Assert.That(result.CheckStatusDetails, Does.Not.Contain("skipped")); }); - // Verify that the PowerShell script was actually called _processHelperMock.Verify(x => x.Run( It.Is(p => IsPowerShellCommand(p) && @@ -381,7 +367,6 @@ public async Task TestCheckAotCompatAsyncWithoutOptOutRunsCheck() } finally { - // Cleanup try { Directory.Delete(testPackagePath, true); @@ -398,11 +383,9 @@ public async Task TestCheckAotCompatAsyncOptOutCaseInsensitive() SetupSuccessfulDotNetVersionCheck(); SetupGitRepoDiscovery(); - // Create a temporary directory structure for the test var testPackagePath = Path.Combine(_repoRoot, "sdk", "testservice", "Azure.TestService"); Directory.CreateDirectory(testPackagePath); - // Create a .csproj file with AotCompatOptOut in different case combinations var csprojPath = Path.Combine(testPackagePath, "Azure.TestService.csproj"); var csprojContent = @" @@ -425,7 +408,6 @@ public async Task TestCheckAotCompatAsyncOptOutCaseInsensitive() } finally { - // Cleanup try { Directory.Delete(testPackagePath, true); } catch { } } } @@ -436,11 +418,9 @@ public async Task TestCheckAotCompatAsyncNoCsprojFileRunsCheck() SetupSuccessfulDotNetVersionCheck(); SetupGitRepoDiscovery(); - // Create a temporary directory structure for the test without any .csproj file var testPackagePath = Path.Combine(_repoRoot, "sdk", "testservice", "Azure.TestService"); Directory.CreateDirectory(testPackagePath); - // Create the expected script path and ensure the directory exists var scriptPath = Path.Combine(_repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); File.WriteAllText(scriptPath, "# Mock PowerShell script"); @@ -467,7 +447,6 @@ public async Task TestCheckAotCompatAsyncNoCsprojFileRunsCheck() Assert.That(result.CheckStatusDetails, Does.Not.Contain("skipped")); }); - // Verify that the PowerShell script was actually called since no opt-out was found _processHelperMock.Verify(x => x.Run( It.Is(p => IsPowerShellCommand(p) && @@ -476,7 +455,6 @@ public async Task TestCheckAotCompatAsyncNoCsprojFileRunsCheck() } finally { - // Cleanup try { Directory.Delete(testPackagePath, true); diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs index c64cdedffd3..67d38bcf47b 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs @@ -15,6 +15,8 @@ public class DotNetLanguageSpecificChecks : ILanguageSpecificChecks private const string DotNetCommand = "dotnet"; private const string PowerShellCommand = "pwsh"; private const string RequiredDotNetVersion = "9.0.102"; // TODO - centralize this as part of env setup tool + private static readonly TimeSpan CodeChecksTimeout = TimeSpan.FromMinutes(6); + private static readonly TimeSpan AotCompatTimeout = TimeSpan.FromMinutes(5); public DotNetLanguageSpecificChecks( IProcessHelper processHelper, @@ -48,7 +50,6 @@ public async Task CheckGeneratedCodeAsync(string packagePath, var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); var scriptPath = Path.Combine(repoRoot, "eng", "scripts", "CodeChecks.ps1"); - if (!File.Exists(scriptPath)) { _logger.LogError("Code checks script not found at: {ScriptPath}", scriptPath); @@ -56,9 +57,7 @@ public async Task CheckGeneratedCodeAsync(string packagePath, } var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-SpellCheckPublicApiSurface" }; - - var timeout = TimeSpan.FromMinutes(6); - var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: repoRoot), ct); + var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: CodeChecksTimeout, workingDirectory: repoRoot), ct); if (result.ExitCode == 0) { @@ -67,14 +66,14 @@ public async Task CheckGeneratedCodeAsync(string packagePath, } else { - _logger.LogWarning("Generated code checks failed with exit code {ExitCode}", result.ExitCode); + _logger.LogWarning("Generated code checks for package at {PackagePath} failed with exit code {ExitCode}", packagePath, result.ExitCode); return new CLICheckResponse(result.ExitCode, result.Output, "Generated code checks failed"); } } catch (Exception ex) { - _logger.LogError(ex, "{MethodName} failed with an exception", nameof(CheckGeneratedCodeAsync)); - return new CLICheckResponse(1, "", $"{nameof(CheckGeneratedCodeAsync)} failed with an exception: {ex.Message}"); + _logger.LogError(ex, "Error running generated code checks at {PackagePath}", packagePath); + return new CLICheckResponse(1, "", $"Error running generated code checks: {ex.Message}"); } } @@ -108,16 +107,15 @@ public async Task CheckAotCompatAsync(string packagePath, bool var repoRoot = _gitHelper.DiscoverRepoRoot(packagePath); var scriptPath = Path.Combine(repoRoot, "eng", "scripts", "compatibility", "Check-AOT-Compatibility.ps1"); - var workingDirectory = Path.Combine(repoRoot, "eng", "scripts", "compatibility"); - if (!File.Exists(scriptPath)) { _logger.LogError("AOT compatibility script not found at: {ScriptPath}", scriptPath); return new CLICheckResponse(1, "", $"AOT compatibility script not found at: {scriptPath}"); } + var workingDirectory = Path.Combine(repoRoot, "eng", "scripts", "compatibility"); var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-PackageName", packageName }; - var timeout = TimeSpan.FromMinutes(6); + var timeout = AotCompatTimeout; var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: workingDirectory), ct); if (result.ExitCode == 0) @@ -133,8 +131,8 @@ public async Task CheckAotCompatAsync(string packagePath, bool } catch (Exception ex) { - _logger.LogError(ex, "{MethodName} failed with an exception", nameof(CheckAotCompatAsync)); - return new CLICheckResponse(1, "", $"{nameof(CheckAotCompatAsync)} failed with an exception: {ex.Message}"); + _logger.LogError(ex, "Error running AOT compatibility check at {PackagePath}", packagePath); + return new CLICheckResponse(1, "", $"Error running AOT compatibility check: {ex.Message}"); } } From 291f1a324235d64f391e5e25423d79a1f116666d Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Thu, 16 Oct 2025 18:10:56 -0700 Subject: [PATCH 13/16] add additional tests --- .../DotNetLanguageSpecificChecksTests.cs | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs index 8fe81ea5077..d8661e90c48 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs @@ -32,7 +32,7 @@ public void SetUp() private void SetupSuccessfulDotNetVersionCheck() { - var versionOutput = $"9.0.100 [C:\\Program Files\\dotnet\\sdk]\n{RequiredDotNetVersion} [C:\\Program Files\\dotnet\\sdk]"; + var versionOutput = $"9.0.102 [C:\\Program Files\\dotnet\\sdk]\n{RequiredDotNetVersion} [C:\\Program Files\\dotnet\\sdk]"; var processResult = new ProcessResult { ExitCode = 0 }; processResult.AppendStdout(versionOutput); @@ -71,6 +71,28 @@ public async Task TestCheckGeneratedCodeAsyncDotNetNotInstalledReturnsError() }); } + [Test] + public async Task TestCheckGeneratedCodeAsyncDotNetVersionTooLowReturnsError() + { + var versionOutput = "6.0.427 [C:\\Program Files\\dotnet\\sdk]\n8.0.404 [C:\\Program Files\\dotnet\\sdk]"; + var processResult = new ProcessResult { ExitCode = 0 }; + processResult.AppendStdout(versionOutput); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => IsDotNetListSdksCommand(p)), + It.IsAny())) + .ReturnsAsync(processResult); + + var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, ct: CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.ResponseError, Does.Contain(".NET SDK version 8.0.404 is below minimum requirement of 9.0.102")); + }); + } + [Test] public async Task TestCheckGeneratedCodeAsyncInvalidPackagePathReturnsError() { @@ -187,6 +209,28 @@ public async Task TestCheckAotCompatAsyncDotNetNotInstalledReturnsError() }); } + [Test] + public async Task TestCheckAotCompatAsyncDotNetVersionTooLowReturnsError() + { + var versionOutput = "6.0.427 [C:\\Program Files\\dotnet\\sdk]\n8.0.404 [C:\\Program Files\\dotnet\\sdk]"; + var processResult = new ProcessResult { ExitCode = 0 }; + processResult.AppendStdout(versionOutput); + + _processHelperMock + .Setup(x => x.Run( + It.Is(p => IsDotNetListSdksCommand(p)), + It.IsAny())) + .ReturnsAsync(processResult); + + var result = await _languageChecks.CheckAotCompatAsync(_packagePath, ct: CancellationToken.None); + + Assert.Multiple(() => + { + Assert.That(result.ExitCode, Is.EqualTo(1)); + Assert.That(result.ResponseError, Does.Contain(".NET SDK version 8.0.404 is below minimum requirement of 9.0.102")); + }); + } + [Test] public async Task TestCheckAotCompatAsyncInvalidPackagePathReturnsError() { From b9e1ceb4c044abdc9400722e436c7e77a5094c20 Mon Sep 17 00:00:00 2001 From: Maddy Heaps <66138537+m-redding@users.noreply.github.com> Date: Thu, 16 Oct 2025 18:27:20 -0700 Subject: [PATCH 14/16] Update tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Services/Languages/ILanguageSpecificChecks.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs index f8a93fe6264..f5c860e5b0b 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs @@ -77,6 +77,8 @@ Task CheckGeneratedCodeAsync(string packagePath, bool fixCheck return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); } + /// + /// Gets the SDK package name for the specified package using language-specific rules. /// /// Repository root path /// Package path From b14e77953601c8660811bf771b1fab8bd73c4ae8 Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Fri, 17 Oct 2025 16:08:05 -0700 Subject: [PATCH 15/16] feedback --- .../DotNetLanguageSpecificChecksTests.cs | 93 ++++++++----------- .../Models/PackageCheckType.cs | 2 +- .../Languages/DotNetLanguageSpecificChecks.cs | 20 ++-- .../Languages/ILanguageSpecificChecks.cs | 22 ++--- .../Services/Languages/LanguageChecks.cs | 17 ++-- .../Tools/Package/PackageCheckTool.cs | 9 +- 6 files changed, 75 insertions(+), 88 deletions(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs index d8661e90c48..42712c1f704 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Services/Languages/DotNetLanguageSpecificChecksTests.cs @@ -10,6 +10,7 @@ internal class DotNetLanguageSpecificChecksTests { private Mock _processHelperMock = null!; private Mock _gitHelperMock = null!; + private Mock _powerShellHelperMock = null!; private DotNetLanguageSpecificChecks _languageChecks = null!; private string _packagePath = null!; private string _repoRoot = null!; @@ -20,9 +21,11 @@ public void SetUp() { _processHelperMock = new Mock(); _gitHelperMock = new Mock(); + _powerShellHelperMock = new Mock(); _languageChecks = new DotNetLanguageSpecificChecks( _processHelperMock.Object, + _powerShellHelperMock.Object, _gitHelperMock.Object, NullLogger.Instance); @@ -62,7 +65,7 @@ public async Task TestCheckGeneratedCodeAsyncDotNetNotInstalledReturnsError() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCode(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -84,7 +87,7 @@ public async Task TestCheckGeneratedCodeAsyncDotNetVersionTooLowReturnsError() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCode(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -99,7 +102,7 @@ public async Task TestCheckGeneratedCodeAsyncInvalidPackagePathReturnsError() SetupSuccessfulDotNetVersionCheck(); var invalidPath = "/tmp/not-in-sdk-folder"; - var result = await _languageChecks.CheckGeneratedCodeAsync(invalidPath, ct: CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCode(invalidPath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -121,17 +124,16 @@ public async Task TestCheckGeneratedCodeAsyncSuccessReturnsSuccess() var processResult = new ProcessResult { ExitCode = 0 }; processResult.AppendStdout("All checks passed successfully!"); - _processHelperMock + _powerShellHelperMock .Setup(x => x.Run( - It.Is(p => - IsPowerShellCommand(p) && - p.Args.Any(a => a.Contains("CodeChecks.ps1"))), + It.Is(p => p.ScriptPath != null && + p.ScriptPath.Contains("CodeChecks.ps1")), It.IsAny())) .ReturnsAsync(processResult); try { - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCode(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -166,11 +168,10 @@ public async Task TestCheckGeneratedCodeAsyncSpellCheckFailureReturnsError(bool Directory.CreateDirectory(Path.GetDirectoryName(scriptPath)!); File.WriteAllText(scriptPath, "# Mock PowerShell script"); - _processHelperMock + _powerShellHelperMock .Setup(x => x.Run( - It.Is(p => - IsPowerShellCommand(p) && - p.Args.Any(a => a.Contains("CodeChecks.ps1"))), + It.Is(p => p.ScriptPath != null && + p.ScriptPath.Contains("CodeChecks.ps1")), It.IsAny())) .ReturnsAsync(() => { @@ -179,7 +180,7 @@ public async Task TestCheckGeneratedCodeAsyncSpellCheckFailureReturnsError(bool return processResult; }); - var result = await _languageChecks.CheckGeneratedCodeAsync(_packagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckGeneratedCode(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -200,7 +201,7 @@ public async Task TestCheckAotCompatAsyncDotNetNotInstalledReturnsError() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.CheckAotCompatAsync(_packagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckAotCompat(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -222,7 +223,7 @@ public async Task TestCheckAotCompatAsyncDotNetVersionTooLowReturnsError() It.IsAny())) .ReturnsAsync(processResult); - var result = await _languageChecks.CheckAotCompatAsync(_packagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckAotCompat(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -237,7 +238,7 @@ public async Task TestCheckAotCompatAsyncInvalidPackagePathReturnsError() SetupSuccessfulDotNetVersionCheck(); var invalidPath = "/tmp/not-in-sdk-folder"; - var result = await _languageChecks.CheckAotCompatAsync(invalidPath, ct: CancellationToken.None); + var result = await _languageChecks.CheckAotCompat(invalidPath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -259,17 +260,16 @@ public async Task TestCheckAotCompatAsyncSuccessReturnsSuccess() var processResult = new ProcessResult { ExitCode = 0 }; processResult.AppendStdout("AOT compatibility check passed!"); - _processHelperMock + _powerShellHelperMock .Setup(x => x.Run( - It.Is(p => - IsPowerShellCommand(p) && - p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), + It.Is(p => p.ScriptPath != null && + p.ScriptPath.Contains("Check-AOT-Compatibility.ps1")), It.IsAny())) .ReturnsAsync(processResult); try { - var result = await _languageChecks.CheckAotCompatAsync(_packagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckAotCompat(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -300,17 +300,16 @@ public async Task TestCheckAotCompatAsyncAotWarningsReturnsError() var processResult = new ProcessResult { ExitCode = 1 }; processResult.AppendStdout(errorMessage); - _processHelperMock + _powerShellHelperMock .Setup(x => x.Run( - It.Is(p => - IsPowerShellCommand(p) && - p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), + It.Is(p => p.ScriptPath != null && + p.ScriptPath.Contains("Check-AOT-Compatibility.ps1")), It.IsAny())) .ReturnsAsync(processResult); try { - var result = await _languageChecks.CheckAotCompatAsync(_packagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckAotCompat(_packagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -345,7 +344,7 @@ public async Task TestCheckAotCompatAsyncWithOptOutReturnsSkipped() try { - var result = await _languageChecks.CheckAotCompatAsync(testPackagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckAotCompat(testPackagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -384,17 +383,16 @@ public async Task TestCheckAotCompatAsyncWithoutOptOutRunsCheck() var processResult = new ProcessResult { ExitCode = 0 }; processResult.AppendStdout("AOT compatibility check passed!"); - _processHelperMock + _powerShellHelperMock .Setup(x => x.Run( - It.Is(p => - IsPowerShellCommand(p) && - p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), + It.Is(p => p.ScriptPath != null && + p.ScriptPath.Contains("Check-AOT-Compatibility.ps1")), It.IsAny())) .ReturnsAsync(processResult); try { - var result = await _languageChecks.CheckAotCompatAsync(testPackagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckAotCompat(testPackagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -402,12 +400,6 @@ public async Task TestCheckAotCompatAsyncWithoutOptOutRunsCheck() Assert.That(result.CheckStatusDetails, Does.Contain("AOT compatibility check passed")); Assert.That(result.CheckStatusDetails, Does.Not.Contain("skipped")); }); - - _processHelperMock.Verify(x => x.Run( - It.Is(p => - IsPowerShellCommand(p) && - p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), - It.IsAny()), Times.Once); } finally { @@ -441,7 +433,7 @@ public async Task TestCheckAotCompatAsyncOptOutCaseInsensitive() try { - var result = await _languageChecks.CheckAotCompatAsync(testPackagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckAotCompat(testPackagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -472,17 +464,16 @@ public async Task TestCheckAotCompatAsyncNoCsprojFileRunsCheck() var processResult = new ProcessResult { ExitCode = 0 }; processResult.AppendStdout("AOT compatibility check passed!"); - _processHelperMock + _powerShellHelperMock .Setup(x => x.Run( - It.Is(p => - IsPowerShellCommand(p) && - p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), + It.Is(p => p.ScriptPath != null && + p.ScriptPath.Contains("Check-AOT-Compatibility.ps1")), It.IsAny())) .ReturnsAsync(processResult); try { - var result = await _languageChecks.CheckAotCompatAsync(testPackagePath, ct: CancellationToken.None); + var result = await _languageChecks.CheckAotCompat(testPackagePath, ct: CancellationToken.None); Assert.Multiple(() => { @@ -491,10 +482,9 @@ public async Task TestCheckAotCompatAsyncNoCsprojFileRunsCheck() Assert.That(result.CheckStatusDetails, Does.Not.Contain("skipped")); }); - _processHelperMock.Verify(x => x.Run( - It.Is(p => - IsPowerShellCommand(p) && - p.Args.Any(a => a.Contains("Check-AOT-Compatibility.ps1"))), + _powerShellHelperMock.Verify(x => x.Run( + It.Is(p => p.ScriptPath != null && + p.ScriptPath.Contains("Check-AOT-Compatibility.ps1")), It.IsAny()), Times.Once); } finally @@ -519,12 +509,5 @@ private static bool IsDotNetListSdksCommand(ProcessOptions options) => (options.Command == "dotnet" && options.Args.Contains("--list-sdks")) || (options.Command == "cmd.exe" && options.Args.Contains("dotnet") && options.Args.Contains("--list-sdks")); - /// - /// Checks if the ProcessOptions represents a PowerShell command. - /// Handles both Unix (pwsh) and Windows (pwsh) patterns. - /// - private static bool IsPowerShellCommand(ProcessOptions options) => - options.Command == "pwsh" || options.Command == "powershell"; - #endregion } diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Models/PackageCheckType.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Models/PackageCheckType.cs index 66114f50c34..51c140e97db 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Models/PackageCheckType.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Models/PackageCheckType.cs @@ -53,7 +53,7 @@ public enum PackageCheckType /// /// .NET validation for generated code. /// - GeneratedCodeChecks + GeneratedCodeChecks, /// /// Validate samples diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs index 67d38bcf47b..b0e24adeafd 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/DotNetLanguageSpecificChecks.cs @@ -11,24 +11,26 @@ public class DotNetLanguageSpecificChecks : ILanguageSpecificChecks { private readonly IProcessHelper _processHelper; private readonly IGitHelper _gitHelper; + private readonly IPowershellHelper _powershellHelper; private readonly ILogger _logger; private const string DotNetCommand = "dotnet"; - private const string PowerShellCommand = "pwsh"; private const string RequiredDotNetVersion = "9.0.102"; // TODO - centralize this as part of env setup tool private static readonly TimeSpan CodeChecksTimeout = TimeSpan.FromMinutes(6); private static readonly TimeSpan AotCompatTimeout = TimeSpan.FromMinutes(5); public DotNetLanguageSpecificChecks( IProcessHelper processHelper, + IPowershellHelper powershellHelper, IGitHelper gitHelper, ILogger logger) { _processHelper = processHelper; + _powershellHelper = powershellHelper; _gitHelper = gitHelper; _logger = logger; } - public async Task CheckGeneratedCodeAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) + public async Task CheckGeneratedCode(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) { try { @@ -57,7 +59,8 @@ public async Task CheckGeneratedCodeAsync(string packagePath, } var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-SpellCheckPublicApiSurface" }; - var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: CodeChecksTimeout, workingDirectory: repoRoot), ct); + var options = new PowershellOptions(scriptPath, args, workingDirectory: repoRoot, timeout: CodeChecksTimeout); + var result = await _powershellHelper.Run(options, ct); if (result.ExitCode == 0) { @@ -77,7 +80,7 @@ public async Task CheckGeneratedCodeAsync(string packagePath, } } - public async Task CheckAotCompatAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) + public async Task CheckAotCompat(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) { try { @@ -98,7 +101,7 @@ public async Task CheckAotCompatAsync(string packagePath, bool return new CLICheckResponse(1, "", "Failed to determine service directory or package name from the provided package path."); } - var isAotOptedOut = CheckAotCompatOptOut(packagePath, packageName); + var isAotOptedOut = await CheckAotCompatOptOut(packagePath, packageName, ct); if (isAotOptedOut) { _logger.LogInformation("AOT compatibility check skipped - AotCompatOptOut is set to true in project file"); @@ -116,7 +119,8 @@ public async Task CheckAotCompatAsync(string packagePath, bool var workingDirectory = Path.Combine(repoRoot, "eng", "scripts", "compatibility"); var args = new[] { scriptPath, "-ServiceDirectory", serviceDirectory, "-PackageName", packageName }; var timeout = AotCompatTimeout; - var result = await _processHelper.Run(new(PowerShellCommand, args, timeout: timeout, workingDirectory: workingDirectory), ct); + var options = new PowershellOptions(scriptPath, args, workingDirectory: workingDirectory, timeout: timeout); + var result = await _powershellHelper.Run(options, ct); if (result.ExitCode == 0) { @@ -215,7 +219,7 @@ private async ValueTask VerifyDotnetVersion() return packageName; } - private bool CheckAotCompatOptOut(string packagePath, string packageName) + private async ValueTask CheckAotCompatOptOut(string packagePath, string packageName, CancellationToken ct) { try { @@ -230,7 +234,7 @@ private bool CheckAotCompatOptOut(string packagePath, string packageName) return false; } - var projectContent = File.ReadAllText(csprojFile); + var projectContent = await File.ReadAllTextAsync(csprojFile, ct); var hasAotOptOut = projectContent.Contains("true", StringComparison.OrdinalIgnoreCase); diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs index 07dbb5b1833..939bb29c969 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/ILanguageSpecificChecks.cs @@ -55,38 +55,38 @@ Task FormatCodeAsync(string packagePath, bool fixCheckErrors = return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); } - /// - /// Checks AOT compatibility for the specific package using language-specific tools. + /// Validate samples for the specific package. /// /// Path to the package directory /// Cancellation token - /// Result of the AOT compatibility check - Task CheckAotCompatAsync(string packagePath, bool fixCheckErrors = false, CancellationToken cancellationToken = default) + /// Result of the sample validation + Task ValidateSamplesAsync(string packagePath, bool fixCheckErrors = false, CancellationToken cancellationToken = default) { return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); } /// - /// Checks generated code for the specific package using language-specific tools. + /// Checks AOT compatibility for the specific package using language-specific tools. /// /// Path to the package directory /// Cancellation token - /// Result of the generated code check - Task CheckGeneratedCodeAsync(string packagePath, bool fixCheckErrors = false, CancellationToken cancellationToken = default) + /// Result of the AOT compatibility check + Task CheckAotCompat(string packagePath, bool fixCheckErrors = false, CancellationToken cancellationToken = default) { return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); } - /// Validate samples for the specific package. + /// + /// Checks generated code for the specific package using language-specific tools. /// /// Path to the package directory /// Cancellation token - /// Result of the sample validation - Task ValidateSamplesAsync(string packagePath, bool fixCheckErrors = false, CancellationToken cancellationToken = default) + /// Result of the generated code check + Task CheckGeneratedCode(string packagePath, bool fixCheckErrors = false, CancellationToken cancellationToken = default) { return Task.FromResult(new CLICheckResponse(1, "", "Not implemented for this language.")); } - + /// /// Gets the SDK package name. /// diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs index 25ea74b725e..133f398fe7a 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs @@ -85,7 +85,7 @@ public interface ILanguageChecks /// Path to the package directory /// Cancellation token /// Result of the AOT compatibility check - Task CheckAotCompatAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default); + Task CheckAotCompat(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default); /// /// Checks generated code for the specific package. @@ -93,7 +93,7 @@ public interface ILanguageChecks /// Path to the package directory /// Cancellation token /// Result of the generated code check - Task CheckGeneratedCodeAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default); + Task CheckGeneratedCode(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default); /// /// Validates samples for the specific package. @@ -232,8 +232,7 @@ public virtual async Task FormatCodeAsync(string packagePath, return await languageSpecificCheck.FormatCodeAsync(packagePath, fixCheckErrors, ct); } - - public virtual async Task CheckAotCompatAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) + public virtual async Task ValidateSamplesAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) { var languageSpecificCheck = await _languageSpecificChecks.Resolve(packagePath); @@ -246,10 +245,10 @@ public virtual async Task CheckAotCompatAsync(string packagePa error: "Unsupported package type" ); } - return await languageSpecificCheck.CheckAotCompatAsync(packagePath, fixCheckErrors, ct); + return await languageSpecificCheck.ValidateSamplesAsync(packagePath, fixCheckErrors, ct); } - public virtual async Task ValidateSamplesAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) + public virtual async Task CheckAotCompat(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) { var languageSpecificCheck = await _languageSpecificChecks.Resolve(packagePath); @@ -262,10 +261,10 @@ public virtual async Task ValidateSamplesAsync(string packageP error: "Unsupported package type" ); } - return await languageSpecificCheck.ValidateSamplesAsync(packagePath, fixCheckErrors, ct); + return await languageSpecificCheck.CheckAotCompat(packagePath, fixCheckErrors, ct); } - public virtual async Task CheckGeneratedCodeAsync(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) + public virtual async Task CheckGeneratedCode(string packagePath, bool fixCheckErrors = false, CancellationToken ct = default) { var languageSpecificCheck = await _languageSpecificChecks.Resolve(packagePath); @@ -279,7 +278,7 @@ public virtual async Task CheckGeneratedCodeAsync(string packa ); } - return await languageSpecificCheck.CheckGeneratedCodeAsync(packagePath, fixCheckErrors, ct); + return await languageSpecificCheck.CheckGeneratedCode(packagePath, fixCheckErrors, ct); } /// diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs index 4f4375839c1..cf9982a6842 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Tools/Package/PackageCheckTool.cs @@ -184,7 +184,7 @@ private async Task RunAllChecks(string packagePath, bool fixCh } // Run AOT compatibility check - var aotCompatResult = await languageChecks.CheckAotCompatAsync(packagePath, fixCheckErrors, ct); + var aotCompatResult = await languageChecks.CheckAotCompat(packagePath, fixCheckErrors, ct); results.Add(aotCompatResult); if (aotCompatResult.ExitCode != 0) { @@ -193,12 +193,13 @@ private async Task RunAllChecks(string packagePath, bool fixCh } // Run generated code check - var generatedCodeResult = await languageChecks.CheckGeneratedCodeAsync(packagePath, fixCheckErrors, ct); + var generatedCodeResult = await languageChecks.CheckGeneratedCode(packagePath, fixCheckErrors, ct); results.Add(generatedCodeResult); if (generatedCodeResult.ExitCode != 0) { overallSuccess = false; failedChecks.Add("Generated Code"); + } // Run sample validation var sampleValidationResult = await languageChecks.ValidateSamplesAsync(packagePath, fixCheckErrors, ct); @@ -355,7 +356,7 @@ private async Task RunCheckGeneratedCode(string packagePath, b { logger.LogInformation("Running generated code checks"); - var result = await languageChecks.CheckGeneratedCodeAsync(packagePath, fixCheckErrors, ct); + var result = await languageChecks.CheckGeneratedCode(packagePath, fixCheckErrors, ct); return result; } @@ -363,7 +364,7 @@ private async Task RunCheckAotCompat(string packagePath, bool { logger.LogInformation("Running AOT compatibility checks"); - var result = await languageChecks.CheckAotCompatAsync(packagePath, fixCheckErrors, ct); + var result = await languageChecks.CheckAotCompat(packagePath, fixCheckErrors, ct); return result; } From 66c787e1f05381f7ba10f6a80d59c4fc6b26fae5 Mon Sep 17 00:00:00 2001 From: Maddy Heaps Date: Fri, 17 Oct 2025 16:10:30 -0700 Subject: [PATCH 16/16] remove unused var --- .../Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs index 133f398fe7a..d9d0ac5d649 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Services/Languages/LanguageChecks.cs @@ -115,7 +115,6 @@ public class LanguageChecks : ILanguageChecks private readonly ILogger _logger; private readonly ILanguageSpecificResolver _languageSpecificChecks; private readonly IMicroagentHostService _microagentHostService; - private const string PowerShellCommand = "pwsh"; public LanguageChecks(IProcessHelper processHelper, INpxHelper npxHelper, IGitHelper gitHelper, ILogger logger, ILanguageSpecificResolver languageSpecificChecks, IMicroagentHostService microagentHostService) {