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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/winapp-CLI/WinApp.Cli.Tests/BaseCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace WinApp.Cli.Tests;

public abstract class BaseCommandTests(bool configPaths = true)
public abstract class BaseCommandTests(bool configPaths = true, bool verboseLogging = true)
{
private protected DirectoryInfo _tempDirectory = null!;
private protected DirectoryInfo _testWinappDirectory = null!;
Expand Down Expand Up @@ -46,7 +46,8 @@ public void SetupBase()
{
b.ClearProviders();
b.AddTextWriterLogger([Console.Out, ConsoleStdOut], [Console.Error, ConsoleStdErr]);
b.SetMinimumLevel(LogLevel.Debug);
// Use Debug level for verbose logging, Information level for non-verbose
b.SetMinimumLevel(verboseLogging ? LogLevel.Debug : LogLevel.Information);
});

_serviceProvider = services.BuildServiceProvider();
Expand Down
70 changes: 66 additions & 4 deletions src/winapp-CLI/WinApp.Cli.Tests/BuildToolsServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using WinApp.Cli.Services;
using WinApp.Cli.Tools;

namespace WinApp.Cli.Tests;
Expand Down Expand Up @@ -159,7 +160,7 @@ public async Task RunBuildToolAsync_WithValidTool_ReturnsOutput()
File.WriteAllText(fakeToolPath, "@echo Hello from fake tool");

// Act
var (stdout, stderr) = await _buildToolsService.RunBuildToolAsync(new GenericTool("echo.cmd"), "", TestContext.CancellationToken);
var (stdout, stderr) = await _buildToolsService.RunBuildToolAsync(new GenericTool("echo.cmd"), "", true, TestContext.CancellationToken);

// Assert
Assert.Contains("Hello from fake tool", stdout);
Expand All @@ -175,7 +176,7 @@ public async Task RunBuildToolAsync_WithNonExistentTool_ThrowsFileNotFoundExcept
// Act & Assert
await Assert.ThrowsExactlyAsync<FileNotFoundException>(async () =>
{
await _buildToolsService.RunBuildToolAsync(new GenericTool("nonexistent.exe"), "", TestContext.CancellationToken);
await _buildToolsService.RunBuildToolAsync(new GenericTool("nonexistent.exe"), "", true, TestContext.CancellationToken);
});
}

Expand Down Expand Up @@ -322,7 +323,7 @@ public async Task RunBuildToolAsync_WithNoExistingPackage_AutoInstallsAndRuns()
{
// Create a simple batch command that outputs something
// This will either succeed (if BuildTools installs successfully) or throw an exception
await _buildToolsService.RunBuildToolAsync(new GenericTool("echo.cmd"), "test", TestContext.CancellationToken);
await _buildToolsService.RunBuildToolAsync(new GenericTool("echo.cmd"), "test", true, TestContext.CancellationToken);

// If we reach here, the auto-installation worked - test passes
}
Expand All @@ -349,10 +350,71 @@ public async Task RunBuildToolAsync_WithExistingTool_RunsDirectly()
File.WriteAllText(batchFile, "@echo Hello from test tool");

// Act
var (stdout, stderr) = await _buildToolsService.RunBuildToolAsync(new GenericTool("test.cmd"), "", TestContext.CancellationToken);
var (stdout, stderr) = await _buildToolsService.RunBuildToolAsync(new GenericTool("test.cmd"), "", true, TestContext.CancellationToken);

// Assert
Assert.Contains("Hello from test tool", stdout);
Assert.AreEqual(string.Empty, stderr.Trim());
}
}

/// <summary>
/// Tests for BuildToolsService with non-verbose logging to test PrintErrorText behavior
/// </summary>
[TestClass]
[DoNotParallelize]
public class BuildToolsServicePrintErrorsTests() : BaseCommandTests(configPaths: true, verboseLogging: false)
{
[TestMethod]
public async Task RunBuildToolAsync_WithPrintErrorsTrue_WritesErrorOutput()
{
// Arrange - Create a batch file that fails with exit code 1 and writes to stderr
var packagesDir = Path.Combine(_testCacheDirectory.FullName, "packages");
var buildToolsPackageDir = Path.Combine(packagesDir, "Microsoft.Windows.SDK.BuildTools.10.0.26100.1");
var binDir = Path.Combine(buildToolsPackageDir, "bin", "10.0.26100.0", "x64");
Directory.CreateDirectory(binDir);

var failingTool = Path.Combine(binDir, "failing.cmd");
File.WriteAllText(failingTool, "@echo Error message to stderr 1>&2\r\n@exit /b 1");

// Act: Run with printErrors=true - error SHOULD be printed via PrintErrorText
var exception = await Assert.ThrowsExactlyAsync<BuildToolsService.InvalidBuildToolException>(async () =>
{
await _buildToolsService.RunBuildToolAsync(new GenericTool("failing.cmd"), "", printErrors: true, TestContext.CancellationToken);
});

// Verify the exception captures the error info
Assert.Contains("Error message to stderr", exception.Stderr);

// Verify that error-level log output occurred when printErrors=true
// (PrintErrorText uses LogError which goes to stderr)
var stdErrOutput = ConsoleStdErr.ToString();
Assert.Contains("Error message to stderr", stdErrOutput, "Error output should be printed when printErrors is true");
}

[TestMethod]
public async Task RunBuildToolAsync_WithPrintErrorsFalse_DoesNotWriteErrorOutput()
{
// Arrange - Create a batch file that fails with exit code 1 and writes to stderr
var packagesDir = Path.Combine(_testCacheDirectory.FullName, "packages");
var buildToolsPackageDir = Path.Combine(packagesDir, "Microsoft.Windows.SDK.BuildTools.10.0.26100.1");
var binDir = Path.Combine(buildToolsPackageDir, "bin", "10.0.26100.0", "x64");
Directory.CreateDirectory(binDir);

var failingTool = Path.Combine(binDir, "failing.cmd");
File.WriteAllText(failingTool, "@echo Error message to stderr 1>&2\r\n@exit /b 1");

// Act: Run with printErrors=false - error should NOT be printed via PrintErrorText
var exception = await Assert.ThrowsExactlyAsync<BuildToolsService.InvalidBuildToolException>(async () =>
{
await _buildToolsService.RunBuildToolAsync(new GenericTool("failing.cmd"), "", printErrors: false, TestContext.CancellationToken);
});

// Verify the exception still captures the error info
Assert.Contains("Error message to stderr", exception.Stderr);

// Verify that NO error-level log output occurred when printErrors=false
var stdErrOutput = ConsoleStdErr.ToString();
Assert.DoesNotContain("Error message to stderr", stdErrOutput, "Error output should NOT be printed when printErrors is false");
}
}
5 changes: 3 additions & 2 deletions src/winapp-CLI/WinApp.Cli/Services/BuildToolsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,10 @@ public async Task<FileInfo> EnsureBuildToolAvailableAsync(string toolName, Cance
/// </summary>
/// <param name="tool">The tool to execute</param>
/// <param name="arguments">Arguments to pass to the tool</param>
/// <param name="printErrors">Whether to print errors using the tool's PrintErrorText method</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Tuple containing (stdout, stderr)</returns>
public async Task<(string stdout, string stderr)> RunBuildToolAsync(Tool tool, string arguments, CancellationToken cancellationToken = default)
public async Task<(string stdout, string stderr)> RunBuildToolAsync(Tool tool, string arguments, bool printErrors, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

Expand Down Expand Up @@ -328,7 +329,7 @@ public async Task<FileInfo> EnsureBuildToolAvailableAsync(string toolName, Cance
{
// Print tool-specific error output when not in verbose mode
// In verbose mode, all output is already visible via LogDebug above
if (!logger.IsEnabled(LogLevel.Debug))
if (!logger.IsEnabled(LogLevel.Debug) && printErrors)
{
tool.PrintErrorText(stdout, stderr, logger);
}
Expand Down
5 changes: 3 additions & 2 deletions src/winapp-CLI/WinApp.Cli/Services/IBuildToolsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ internal interface IBuildToolsService
Task<FileInfo> EnsureBuildToolAvailableAsync(string toolName, CancellationToken cancellationToken = default);

Task<DirectoryInfo?> EnsureBuildToolsAsync(bool forceLatest = false, CancellationToken cancellationToken = default);

/// <summary>
/// Execute a build tool with the specified arguments
/// </summary>
/// <param name="tool">The tool to execute</param>
/// <param name="arguments">Arguments to pass to the tool</param>
/// <param name="printErrors">Whether to print errors using the tool's PrintErrorText method</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Tuple containing (stdout, stderr)</returns>
Task<(string stdout, string stderr)> RunBuildToolAsync(Tool tool, string arguments, CancellationToken cancellationToken = default);
Task<(string stdout, string stderr)> RunBuildToolAsync(Tool tool, string arguments, bool printErrors = true, CancellationToken cancellationToken = default);
}
10 changes: 5 additions & 5 deletions src/winapp-CLI/WinApp.Cli/Services/MsixService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ private async Task EmbedManifestFileToExeAsync(
logger.LogDebug("Merging with existing manifest using mt.exe...");

// Use mt.exe to merge existing manifest with new manifest
await RunMtToolAsync($@"-manifest ""{tempManifestPath}"" ""{manifestPath}"" -out:""{mergedManifestPath}""", cancellationToken);
await RunMtToolAsync($@"-manifest ""{tempManifestPath}"" ""{manifestPath}"" -out:""{mergedManifestPath}""", true, cancellationToken);
}
else
{
Expand All @@ -455,7 +455,7 @@ private async Task EmbedManifestFileToExeAsync(
logger.LogDebug("Embedding merged manifest into executable...");

// Update the executable with merged manifest
await RunMtToolAsync($@"-manifest ""{mergedManifestPath}"" -outputresource:""{exePath}"";#1", cancellationToken);
await RunMtToolAsync($@"-manifest ""{mergedManifestPath}"" -outputresource:""{exePath}"";#1", true, cancellationToken);

logger.LogDebug("{UISymbol} Successfully embedded manifest into: {ExecutablePath}", UiSymbols.Check, exePath);
}
Expand All @@ -479,7 +479,7 @@ private async Task<bool> TryExtractManifestFromExeAsync(FileInfo exePath, FileIn
bool hasExistingManifest = false;
try
{
await RunMtToolAsync($@"-inputresource:""{exePath}"";#1 -out:""{tempManifestPath}""", cancellationToken);
await RunMtToolAsync($@"-inputresource:""{exePath}"";#1 -out:""{tempManifestPath}""", false, cancellationToken);
tempManifestPath.Refresh();
hasExistingManifest = tempManifestPath.Exists;
}
Expand Down Expand Up @@ -1133,10 +1133,10 @@ private async Task CreateMsixPackageFromFolderAsync(DirectoryInfo inputFolder, F
await buildToolsService.RunBuildToolAsync(new MakeAppxTool(), makeappxArguments, cancellationToken: cancellationToken);
}

private async Task RunMtToolAsync(string arguments, CancellationToken cancellationToken = default)
private async Task RunMtToolAsync(string arguments, bool printErrors, CancellationToken cancellationToken = default)
{
// Use BuildToolsService to run mt.exe
await buildToolsService.RunBuildToolAsync(new GenericTool("mt.exe"), arguments, cancellationToken: cancellationToken);
await buildToolsService.RunBuildToolAsync(new GenericTool("mt.exe"), arguments, printErrors, cancellationToken: cancellationToken);
}

private static void TryDeleteFile(FileInfo path)
Expand Down