From 3e6c888ef2f67ff65bfaa7e9e3da57dcdd9e4fe9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:02:39 +0000 Subject: [PATCH 1/2] Initial plan From bf563fe004c75290e1f2ef857dca009db2edf86d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:21:20 +0000 Subject: [PATCH 2/2] Apply logging fixes from PR #33624 (excluding Common.targets) - Update ToolRunner.cs to use ITestOutputHelper instead of Console.WriteLine - Update DotnetInternal.cs to pass ITestOutputHelper, add OutputBuildErrorsFromBinLog - Update XHarness.cs to pass ITestOutputHelper to all methods - Update Adb.cs and Emulator.cs (Android) to use ITestOutputHelper - Update Simulator.cs (Apple) to store ITestOutputHelper as field - Update BuildWarningsUtilities.cs with new OutputBuildErrorsFromBinLog method - Update all test files to pass output: _output to DotnetInternal and XHarness calls - Add ShowLiveOutput property to Microsoft.Maui.IntegrationTests.csproj Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com> --- .../AOTTemplateTest.cs | 8 +- .../Android/Adb.cs | 27 +++--- .../Android/Emulator.cs | 37 +++---- .../AndroidTemplateTests.cs | 6 +- .../Apple/Simulator.cs | 23 +++-- .../AppleTemplateTests.cs | 6 +- .../BlazorTemplateTest.cs | 6 +- .../MacTemplateTest.cs | 12 +-- .../Microsoft.Maui.IntegrationTests.csproj | 2 + .../MultiProjectTemplateTest.cs | 16 +-- .../ResizetizerTests.cs | 6 +- .../SampleTests.cs | 2 +- .../SimpleTemplateTest.cs | 38 ++++---- .../Utilities/BuildWarningsUtilities.cs | 48 ++++++++- .../Utilities/DotnetInternal.cs | 97 +++++++++++-------- .../Utilities/ToolRunner.cs | 12 ++- .../Utilities/XHarness.cs | 35 ++++--- .../WindowsTemplateTest.cs | 24 ++--- 18 files changed, 242 insertions(+), 163 deletions(-) diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AOTTemplateTest.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AOTTemplateTest.cs index a088b35a3766..d571dd125081 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AOTTemplateTest.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AOTTemplateTest.cs @@ -28,13 +28,13 @@ public void PublishNativeAOT(string id, string framework, string runtimeIdentifi var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent), + Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent, output: _output), $"Unable to create template {id}. Check test output for errors."); var extendedBuildProps = isWindowsFramework ? PrepareNativeAotBuildPropsWindows(runtimeIdentifier) : PrepareNativeAotBuildProps(); string binLogFilePath = $"publish-{DateTime.UtcNow.ToFileTimeUtc()}.binlog"; - Assert.True(DotnetInternal.Build(projectFile, "Release", framework: framework, properties: extendedBuildProps, runtimeIdentifier: runtimeIdentifier, binlogPath: binLogFilePath), + Assert.True(DotnetInternal.Build(projectFile, "Release", framework: framework, properties: extendedBuildProps, runtimeIdentifier: runtimeIdentifier, binlogPath: binLogFilePath, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); var actualWarnings = BuildWarningsUtilities.ReadNativeAOTWarningsFromBinLog(binLogFilePath); @@ -64,7 +64,7 @@ public void PublishNativeAOTRootAllMauiAssemblies(string id, string framework, s var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent), + Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent, output: _output), $"Unable to create template {id}. Check test output for errors."); var extendedBuildProps = isWindowsFramework ? PrepareNativeAotBuildPropsWindows(runtimeIdentifier) : PrepareNativeAotBuildProps(); @@ -91,7 +91,7 @@ public void PublishNativeAOTRootAllMauiAssemblies(string id, string framework, s """); string binLogFilePath = $"publish-{DateTime.UtcNow.ToFileTimeUtc()}.binlog"; - Assert.True(DotnetInternal.Build(projectFile, "Release", framework: framework, properties: extendedBuildProps, runtimeIdentifier: runtimeIdentifier, binlogPath: binLogFilePath), + Assert.True(DotnetInternal.Build(projectFile, "Release", framework: framework, properties: extendedBuildProps, runtimeIdentifier: runtimeIdentifier, binlogPath: binLogFilePath, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); var actualWarnings = BuildWarningsUtilities.ReadNativeAOTWarningsFromBinLog(binLogFilePath); diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Android/Adb.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Android/Adb.cs index 37f359aa5726..cee13f56c3ec 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Android/Adb.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Android/Adb.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Xunit.Abstractions; namespace Microsoft.Maui.IntegrationTests.Android { @@ -7,16 +8,16 @@ public static class Adb static readonly string AdbTool = Path.Combine(TestEnvironment.GetAndroidSdkPath(), "platform-tools", "adb"); const int DEFAULT_TIMEOUT = 20; - public static bool WaitForEmulator(int timeout, string deviceId = "") + public static bool WaitForEmulator(int timeout, string deviceId = "", ITestOutputHelper? output = null) { var maxWaitTime = DateTime.UtcNow.AddSeconds(timeout); int currentWaitTime = 0; bool bootCompleted = false; while (DateTime.UtcNow < maxWaitTime && !bootCompleted) { - Console.WriteLine($"Waiting {currentWaitTime}/{timeout} seconds for the emulator to boot up..."); - var adbOutput = RunForOutput(GetArgs("shell getprop sys.boot_completed", deviceId), out _); - Console.WriteLine($"sys.boot_completed: {adbOutput}"); + output?.WriteLine($"Waiting {currentWaitTime}/{timeout} seconds for the emulator to boot up..."); + var adbOutput = RunForOutput(GetArgs("shell getprop sys.boot_completed", deviceId), out _, output: output); + output?.WriteLine($"sys.boot_completed: {adbOutput}"); int.TryParse(adbOutput, out int bootCompletedPropValue); bootCompleted = bootCompletedPropValue == 1; Thread.Sleep(10000); @@ -25,14 +26,14 @@ public static bool WaitForEmulator(int timeout, string deviceId = "") return bootCompleted; } - public static bool KillEmulator(string deviceId = "") + public static bool KillEmulator(string deviceId = "", ITestOutputHelper? output = null) { - return Run(GetArgs("emu kill", deviceId)); + return Run(GetArgs("emu kill", deviceId), output: output); } - public static bool UninstallPackage(string package, string deviceId = "") + public static bool UninstallPackage(string package, string deviceId = "", ITestOutputHelper? output = null) { - return Run(GetArgs($"uninstall {package}", deviceId)); + return Run(GetArgs($"uninstall {package}", deviceId), output: output); } static string GetArgs(string args, string deviceId) @@ -40,18 +41,18 @@ static string GetArgs(string args, string deviceId) return string.IsNullOrEmpty(deviceId) ? args : $"-s {deviceId} " + args; } - public static bool Run(string args, int timeout = DEFAULT_TIMEOUT, string deviceId = "") + public static bool Run(string args, int timeout = DEFAULT_TIMEOUT, string deviceId = "", ITestOutputHelper? output = null) { - RunForOutput(args, out int exitCode, timeout, deviceId); + RunForOutput(args, out int exitCode, timeout, deviceId, output: output); if (exitCode != 0) - Console.WriteLine(exitCode); + output?.WriteLine(exitCode.ToString()); return exitCode == 0; } - public static string RunForOutput(string args, out int exitCode, int timeout = DEFAULT_TIMEOUT, string deviceId = "") + public static string RunForOutput(string args, out int exitCode, int timeout = DEFAULT_TIMEOUT, string deviceId = "", ITestOutputHelper? output = null) { - return ToolRunner.Run(AdbTool, GetArgs(args, deviceId), out exitCode, timeoutInSeconds: timeout); + return ToolRunner.Run(AdbTool, GetArgs(args, deviceId), out exitCode, timeoutInSeconds: timeout, output: output); } } diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Android/Emulator.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Android/Emulator.cs index c066e3150087..ab34a5f17f90 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Android/Emulator.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Android/Emulator.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Xunit.Abstractions; namespace Microsoft.Maui.IntegrationTests.Android { @@ -18,7 +19,7 @@ public class Emulator public string Id => $"emulator-{Port}"; public string SystemImageId => $"system-images;android-{ApiLevel};{ImageType};{Abi}"; - public bool AcceptLicenses(out string acceptLicenseOutput) + public bool AcceptLicenses(out string acceptLicenseOutput, ITestOutputHelper? output = null) { acceptLicenseOutput = ToolRunner.Run(new ProcessStartInfo(SdkManagerTool, "--licenses"), out int exitCode, timeoutInSeconds: 30, inputAction: (p) => { @@ -26,61 +27,61 @@ public bool AcceptLicenses(out string acceptLicenseOutput) { p.StandardInput.WriteLine('y'); } - }); + }, output: output); if (exitCode != 0) - Console.WriteLine(acceptLicenseOutput); + output?.WriteLine(acceptLicenseOutput); return exitCode == 0; } - public bool InstallAvd(out string installOutput) + public bool InstallAvd(out string installOutput, ITestOutputHelper? output = null) { - installOutput = ToolRunner.Run(SdkManagerTool, $"\"{SystemImageId}\"", out int exitCode, timeoutInSeconds: 180); + installOutput = ToolRunner.Run(SdkManagerTool, $"\"{SystemImageId}\"", out int exitCode, timeoutInSeconds: 180, output: output); if (exitCode != 0) - Console.WriteLine(installOutput); + output?.WriteLine(installOutput); return exitCode == 0; } - public bool DeleteAvd() + public bool DeleteAvd(ITestOutputHelper? output = null) { - var deleteOutput = ToolRunner.Run(AvdManagerTool, $"delete avd -n {Name}", out int exitCode, timeoutInSeconds: 15); + var deleteOutput = ToolRunner.Run(AvdManagerTool, $"delete avd -n {Name}", out int exitCode, timeoutInSeconds: 15, output: output); return exitCode == 0 || deleteOutput.Contains($"There is no Android Virtual Device named '{Name}'", StringComparison.OrdinalIgnoreCase); } - public bool CreateAvd(bool force = true) + public bool CreateAvd(bool force = true, ITestOutputHelper? output = null) { var createArgs = $"create avd -n {Name} -k \"{SystemImageId}\" -d {DeviceType}"; if (force) createArgs += " -f"; - var createOutput = ToolRunner.Run(AvdManagerTool, createArgs, out int exitCode, timeoutInSeconds: 15); + var createOutput = ToolRunner.Run(AvdManagerTool, createArgs, out int exitCode, timeoutInSeconds: 15, output: output); if (exitCode != 0) - Console.WriteLine(createOutput); + output?.WriteLine(createOutput); return exitCode == 0; } - public bool LaunchAndWaitForAvd(int timeToWaitInSeconds, string logFile) + public bool LaunchAndWaitForAvd(int timeToWaitInSeconds, string logFile, ITestOutputHelper? output = null) { - if (Adb.WaitForEmulator(10, Id)) + if (Adb.WaitForEmulator(10, Id, output: output)) return true; - if (!DeleteAvd()) + if (!DeleteAvd(output: output)) return false; - if (!CreateAvd()) + if (!CreateAvd(output: output)) return false; var launchArgs = $"-verbose -detect-image-hang -port {Port} -avd {Name}"; launchArgs += TestEnvironment.IsRunningOnCI ? " -no-window -no-boot-anim -no-audio -no-snapshot -cache-size 512" : string.Empty; // Emulator process does not stop once the emulator is running, end it after 15 seconds and then begin polling for boot success - Console.WriteLine($"Launching AVD: {Name}..."); - var emulatorOutput = ToolRunner.Run(EmulatorTool, launchArgs, out _, timeoutInSeconds: 15); + output?.WriteLine($"Launching AVD: {Name}..."); + var emulatorOutput = ToolRunner.Run(EmulatorTool, launchArgs, out _, timeoutInSeconds: 15, output: output); File.WriteAllText(logFile, emulatorOutput); - return Adb.WaitForEmulator(timeToWaitInSeconds, Id); + return Adb.WaitForEmulator(timeToWaitInSeconds, Id, output: output); } } diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AndroidTemplateTests.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AndroidTemplateTests.cs index 1faf0936e322..0a632cfb85bf 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AndroidTemplateTests.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AndroidTemplateTests.cs @@ -89,7 +89,7 @@ public void RunOnAndroid(string id, string framework, string config, string? tri var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, framework), + Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); var buildProps = BuildProps; @@ -101,7 +101,7 @@ public void RunOnAndroid(string id, string framework, string config, string? tri AddInstrumentation(projectDir); - Assert.True(DotnetInternal.Build(projectFile, config, target: "Install", framework: $"{framework}-android", properties: BuildProps), + Assert.True(DotnetInternal.Build(projectFile, config, target: "Install", framework: $"{framework}-android", properties: BuildProps, output: _output), $"Project {Path.GetFileName(projectFile)} failed to install. Check test output/attachments for errors."); // Write xh-results to the log directory for artifact collection @@ -109,7 +109,7 @@ public void RunOnAndroid(string id, string framework, string config, string? tri Directory.CreateDirectory(xhResultsDir); testPackage = $"com.companyname.{Path.GetFileName(projectDir).ToLowerInvariant()}"; - Assert.True(XHarness.RunAndroid(testPackage, xhResultsDir, -1), + Assert.True(XHarness.RunAndroid(testPackage, xhResultsDir, -1, output: _output), $"Project {Path.GetFileName(projectFile)} failed to run. Check test output/attachments for errors."); } diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Apple/Simulator.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Apple/Simulator.cs index 26dcd788abfe..3458da5e98fb 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Apple/Simulator.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Apple/Simulator.cs @@ -1,10 +1,17 @@ using System.Text.Json; +using Xunit.Abstractions; namespace Microsoft.Maui.IntegrationTests.Apple { public class Simulator { readonly string XCRunTool = "xcrun"; + readonly ITestOutputHelper? _output; + + public Simulator(ITestOutputHelper? output = null) + { + _output = output; + } string? _xharnessID; @@ -45,12 +52,12 @@ string ResolveXHarnessID() { var versionSuffix = $"{latestVersion.Major}.{latestVersion.Minor}"; var resolvedTarget = $"{baseTarget}_{versionSuffix}"; - Console.WriteLine($"Auto-detected iOS target: {resolvedTarget}"); + _output?.WriteLine($"Auto-detected iOS target: {resolvedTarget}"); return resolvedTarget; } // Fallback to base target (may fail if XHarness can't find a match) - Console.WriteLine($"Warning: Could not auto-detect iOS version, using '{baseTarget}' which may fail."); + _output?.WriteLine($"Warning: Could not auto-detect iOS version, using '{baseTarget}' which may fail."); return baseTarget; } @@ -62,7 +69,7 @@ string ResolveXHarnessID() { try { - var output = ToolRunner.Run(XCRunTool, "simctl list runtimes --json", out int exitCode, timeoutInSeconds: 30); + var output = ToolRunner.Run(XCRunTool, "simctl list runtimes --json", out int exitCode, timeoutInSeconds: 30, output: _output); if (exitCode != 0 || string.IsNullOrEmpty(output)) return null; @@ -99,7 +106,7 @@ string ResolveXHarnessID() } catch (Exception ex) { - Console.WriteLine($"Failed to detect iOS version: {ex.Message}"); + _output?.WriteLine($"Failed to detect iOS version: {ex.Message}"); return null; } } @@ -110,7 +117,7 @@ public string GetUDID() if (!string.IsNullOrEmpty(_udid)) return _udid; - var xharnessOutput = XHarness.GetSimulatorUDID(XHarnessID).Trim(); + var xharnessOutput = XHarness.GetSimulatorUDID(XHarnessID, output: _output).Trim(); // XHarness returns a UDID on success, or an error message on failure. // A valid UDID is a UUID format (e.g., "DE87D078-70D4-47F6-9F21-82612D9D4F7E") @@ -132,7 +139,7 @@ static bool IsValidUDID(string value) public bool Launch() { - var output = ToolRunner.Run(XCRunTool, $"simctl boot {GetUDID()}", out int exitCode, timeoutInSeconds: 30); + var output = ToolRunner.Run(XCRunTool, $"simctl boot {GetUDID()}", out int exitCode, timeoutInSeconds: 30, output: _output); // Exit code 0 = successfully booted // Exit code 149 with "current state: Booted" = already running (also success) return exitCode == 0 || (exitCode == 149 && output.Contains("current state: Booted", StringComparison.Ordinal)); @@ -140,13 +147,13 @@ public bool Launch() public bool Shutdown() { - ToolRunner.Run(XCRunTool, $"simctl shutdown {GetUDID()}", out int exitCode, timeoutInSeconds: 60); + ToolRunner.Run(XCRunTool, $"simctl shutdown {GetUDID()}", out int exitCode, timeoutInSeconds: 60, output: _output); return exitCode == 0; } public bool ShowWindow() { - ToolRunner.Run("open", $"-a Simulator", out int exitCode, timeoutInSeconds: 30); + ToolRunner.Run("open", $"-a Simulator", out int exitCode, timeoutInSeconds: 30, output: _output); return exitCode == 0; } } diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AppleTemplateTests.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AppleTemplateTests.cs index ff0eb74ab63b..707c4f08cc8a 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AppleTemplateTests.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/AppleTemplateTests.cs @@ -100,7 +100,7 @@ void RunOniOS(string id, string config, string framework, RuntimeVariant runtime var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, framework), + Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); var buildProps = BuildProps; @@ -126,7 +126,7 @@ void RunOniOS(string id, string config, string framework, RuntimeVariant runtime buildProps.Add("TrimmerSingleWarn=false"); // Disable trimmer warnings for iOS full trimming builds due to ObjCRuntime issues } - Assert.True(DotnetInternal.Build(projectFile, config, framework: $"{framework}-ios", properties: buildProps, runtimeIdentifier: runtimeIdentifier), + Assert.True(DotnetInternal.Build(projectFile, config, framework: $"{framework}-ios", properties: buildProps, runtimeIdentifier: runtimeIdentifier, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); // Find the .app bundle - it may be in the bin folder with or without a RID subfolder depending on build settings @@ -142,7 +142,7 @@ void RunOniOS(string id, string config, string framework, RuntimeVariant runtime // Let XHarness find the simulator based on target (e.g., ios-simulator-64_18.5). // Don't pass a specific UDID - this gives XHarness full control over the simulator // lifecycle and avoids race conditions with watchdog disabling. - Assert.True(XHarness.RunAppleForTimeout(appFile, xhResultsDir, _simulatorFixture.TestSimulator.XHarnessID), + Assert.True(XHarness.RunAppleForTimeout(appFile, xhResultsDir, _simulatorFixture.TestSimulator.XHarnessID, output: _output), $"Project {Path.GetFileName(projectFile)} failed to run. Check test output/attachments for errors."); } } diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/BlazorTemplateTest.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/BlazorTemplateTest.cs index 0907ac830573..4e5f5e60fa6e 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/BlazorTemplateTest.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/BlazorTemplateTest.cs @@ -68,7 +68,7 @@ public void BuildMauiBlazorWebSolution(string framework, string config, string a _output.WriteLine($"Creating project in {solutionProjectDir}"); - Assert.True(DotnetInternal.New(templateShortName, outputDirectory: solutionProjectDir, framework: framework, additionalDotNetNewParams: additionalDotNetNewParams), + Assert.True(DotnetInternal.New(templateShortName, outputDirectory: solutionProjectDir, framework: framework, additionalDotNetNewParams: additionalDotNetNewParams, output: _output), $"Unable to create template {templateShortName}. Check test output for errors."); _output.WriteLine($"Solution directory: {solutionProjectDir} (exists? {Directory.Exists(solutionProjectDir)})"); @@ -78,11 +78,11 @@ public void BuildMauiBlazorWebSolution(string framework, string config, string a _output.WriteLine($"MAUI app project file: {mauiAppProjectFile} (exists? {File.Exists(mauiAppProjectFile)})"); _output.WriteLine($"Building Blazor Web app: {webAppProjectFile}"); - Assert.True(DotnetInternal.Build(webAppProjectFile, config, target: "", properties: BuildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(webAppProjectFile, config, target: "", properties: BuildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(webAppProjectFile)} failed to build. Check test output/attachments for errors."); _output.WriteLine($"Building .NET MAUI app: {mauiAppProjectFile} props: {buildProps}"); - Assert.True(DotnetInternal.Build(mauiAppProjectFile, config, target: "", properties: buildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(mauiAppProjectFile, config, target: "", properties: buildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(mauiAppProjectFile)} failed to build. Check test output/attachments for errors."); } } diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/MacTemplateTest.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/MacTemplateTest.cs index 33a0958c3980..dd5100b9d589 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/MacTemplateTest.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/MacTemplateTest.cs @@ -18,7 +18,7 @@ public void BuildWithCustomBundleResource(string id, string framework) var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent), + Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent, output: _output), $"Unable to create template {id}. Check test output for errors."); File.WriteAllText(Path.Combine(projectDir, "Resources", "testfile.txt"), "Something here :)"); @@ -35,7 +35,7 @@ public void BuildWithCustomBundleResource(string id, string framework) var extendedBuildProps = BuildProps; extendedBuildProps.Add($"TargetFramework={DotNetCurrent}-{framework}"); - Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: extendedBuildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: extendedBuildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } @@ -65,8 +65,8 @@ private void CheckEntitlementsForMauiBlazorOnMacCatalyst(string id, string confi $"EnableCodeSigning={sign}" }; - Assert.True(DotnetInternal.New(id, projectDir, framework), $"Unable to create template {id}. Check test output for errors."); - Assert.True(DotnetInternal.Build(projectFile, config, framework: $"{framework}-maccatalyst", properties: buildWithCodeSignProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); + Assert.True(DotnetInternal.Build(projectFile, config, framework: $"{framework}-maccatalyst", properties: buildWithCodeSignProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); List expectedEntitlements = @@ -124,8 +124,8 @@ public void CheckPrivacyManifestForiOS(string id, string config, string framewor buildWithCodeSignProps.Add("EnableCodeSigning=true"); } - Assert.True(DotnetInternal.New(id, projectDir, framework), $"Unable to create template {id}. Check test output for errors."); - Assert.True(DotnetInternal.Build(projectFile, config, framework: $"{framework}-ios", properties: buildWithCodeSignProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); + Assert.True(DotnetInternal.Build(projectFile, config, framework: $"{framework}-ios", properties: buildWithCodeSignProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); string manifestLocation = Path.Combine(appLocation, "PrivacyInfo.xcprivacy"); diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Microsoft.Maui.IntegrationTests.csproj b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Microsoft.Maui.IntegrationTests.csproj index 54f8184a964d..5a27d251ccaa 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Microsoft.Maui.IntegrationTests.csproj +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Microsoft.Maui.IntegrationTests.csproj @@ -6,6 +6,8 @@ enable false Major + + true diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/MultiProjectTemplateTest.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/MultiProjectTemplateTest.cs index e41656f54ee6..42aeda29beec 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/MultiProjectTemplateTest.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/MultiProjectTemplateTest.cs @@ -17,7 +17,7 @@ public void BuildMultiProject(string config, string projectName) var name = Path.GetFileName(projectDir); var solutionFile = Path.Combine(projectDir, $"{name}.sln"); - Assert.True(DotnetInternal.New("maui-multiproject", projectDir, DotNetCurrent), + Assert.True(DotnetInternal.New("maui-multiproject", projectDir, DotNetCurrent, output: _output), $"Unable to create template maui-multiproject. Check test output for errors."); // Always remove WinUI project if the project name contains special characters that cause WinRT source generator issues @@ -26,7 +26,7 @@ public void BuildMultiProject(string config, string projectName) if (!TestEnvironment.IsWindows || containsSpecialChars) { - Assert.True(DotnetInternal.Run("sln", $"\"{solutionFile}\" remove \"{projectDir}/{name}.WinUI/{name}.WinUI.csproj\""), + Assert.True(DotnetInternal.Run("sln", $"\"{solutionFile}\" remove \"{projectDir}/{name}.WinUI/{name}.WinUI.csproj\"", output: _output), $"Unable to remove WinUI project from solution. Check test output for errors."); } @@ -34,7 +34,7 @@ public void BuildMultiProject(string config, string projectName) var buildProps = BuildProps; buildProps.Add("ResizetizerErrorOnDuplicateOutputFilename=false"); - Assert.True(DotnetInternal.Build(solutionFile, config, properties: buildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(solutionFile, config, properties: buildProps, msbuildWarningsAsErrors: true, output: _output), $"Solution {name} failed to build. Check test output/attachments for errors."); } @@ -50,16 +50,16 @@ public void BuildMultiProjectSinglePlatform(string config, string platformArg) var name = Path.GetFileName(projectDir); var solutionFile = Path.Combine(projectDir, $"{name}.sln"); - Assert.True(DotnetInternal.New($"maui-multiproject {platformArg}", projectDir, DotNetCurrent), + Assert.True(DotnetInternal.New($"maui-multiproject {platformArg}", projectDir, DotNetCurrent, output: _output), $"Unable to create template maui-multiproject. Check test output for errors."); if (!TestEnvironment.IsWindows) { - Assert.True(DotnetInternal.Run("sln", $"{solutionFile} remove {projectDir}/{name}.WinUI/{name}.WinUI.csproj"), + Assert.True(DotnetInternal.Run("sln", $"{solutionFile} remove {projectDir}/{name}.WinUI/{name}.WinUI.csproj", output: _output), $"Unable to remove WinUI project from solution. Check test output for errors."); } - Assert.True(DotnetInternal.Build(solutionFile, config, properties: BuildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(solutionFile, config, properties: BuildProps, msbuildWarningsAsErrors: true, output: _output), $"Solution {name} failed to build. Check test output/attachments for errors."); } @@ -77,10 +77,10 @@ public void VerifyIncludedPlatformsInSln(string platformArg) var name = Path.GetFileName(projectDir); var solutionFile = Path.Combine(projectDir, $"{name}.sln"); - Assert.True(DotnetInternal.New($"maui-multiproject {platformArg}", projectDir, DotNetCurrent), + Assert.True(DotnetInternal.New($"maui-multiproject {platformArg}", projectDir, DotNetCurrent, output: _output), $"Unable to create template maui-multiproject. Check test output for errors."); - var slnListOutput = DotnetInternal.RunForOutput("sln", $"{solutionFile} list", out int exitCode); + var slnListOutput = DotnetInternal.RunForOutput("sln", $"{solutionFile} list", out int exitCode, output: _output); // Asserts the process completed successfully if (exitCode != 0) diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs index 2323d8286074..fefbe048b1ed 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/ResizetizerTests.cs @@ -34,13 +34,13 @@ public void CollectsAssets(string id, string libid, bool unpackaged) // new app var appDir = Path.Combine(TestDirectory, "theapp"); var appFile = Path.Combine(appDir, $"{Path.GetFileName(appDir)}.csproj"); - Assert.True(DotnetInternal.New(id, appDir, DotNetCurrent), + Assert.True(DotnetInternal.New(id, appDir, DotNetCurrent, output: _output), $"Unable to create template {id}. Check test output for errors."); // new lib var libDir = Path.Combine(TestDirectory, "thelib"); var libFile = Path.Combine(libDir, $"{Path.GetFileName(libDir)}.csproj"); - Assert.True(DotnetInternal.New(libid, libDir, DotNetCurrent), + Assert.True(DotnetInternal.New(libid, libDir, DotNetCurrent, output: _output), $"Unable to create template {libid}. Check test output for errors."); // add a project reference @@ -85,7 +85,7 @@ public void CollectsAssets(string id, string libid, bool unpackaged) """); // build - Assert.True(DotnetInternal.Build(appFile, "Debug", properties: BuildProps), + Assert.True(DotnetInternal.Build(appFile, "Debug", properties: BuildProps, output: _output), $"Project {Path.GetFileName(appFile)} failed to build. Check test output/attachments for errors."); // assert diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/SampleTests.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/SampleTests.cs index 79c051a65af8..a1448d25800a 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/SampleTests.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/SampleTests.cs @@ -42,7 +42,7 @@ public void Build(string relativeProj, string config) "TrimmerSingleWarn=false", }; - Assert.True(DotnetInternal.Build(projectFile, config, properties: sampleProps, binlogPath: binlog), + Assert.True(DotnetInternal.Build(projectFile, config, properties: sampleProps, binlogPath: binlog, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/SimpleTemplateTest.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/SimpleTemplateTest.cs index b726b0a887dd..a79ca922cb88 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/SimpleTemplateTest.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/SimpleTemplateTest.cs @@ -34,7 +34,7 @@ public void Build(string id, string framework, string config, bool shouldPack, s var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, framework, additionalDotNetNewParams), + Assert.True(DotnetInternal.New(id, projectDir, framework, additionalDotNetNewParams, output: _output), $"Unable to create template {id}. Check test output for errors."); @@ -57,7 +57,7 @@ public void Build(string id, string framework, string config, bool shouldPack, s } string target = shouldPack ? "Pack" : ""; - Assert.True(DotnetInternal.Build(projectFile, config, target: target, properties: buildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(projectFile, config, target: target, properties: buildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } @@ -68,14 +68,14 @@ public void InstallPackagesIntoUnsupportedTfmFails(string id, string framework, var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, framework), + Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); FileUtilities.ReplaceInFile(projectFile, "$(MauiVersion)", MauiPackageVersion); - Assert.False(DotnetInternal.Build(projectFile, config, properties: BuildProps, msbuildWarningsAsErrors: true), + Assert.False(DotnetInternal.Build(projectFile, config, properties: BuildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} built, but should not have. Check test output/attachments for why."); } @@ -92,7 +92,7 @@ public void BuildsWithSpecialCharacters(string id, string projectName, string ex var projectDir = Path.Combine(TestDirectory, projectName); var projectFile = Path.Combine(projectDir, $"{projectName}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent), + Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent, output: _output), $"Unable to create template {id}. Check test output for errors."); // libraries do not have application IDs @@ -117,7 +117,7 @@ public void BuildsWithSpecialCharacters(string id, string projectName, string ex Assert.Equal(projectName, appTitle); } - Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: BuildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: BuildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } @@ -140,7 +140,7 @@ public void BuildWithMauiVersion(string id, string framework, string config, boo var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, framework), + Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); if (shouldPack) @@ -167,7 +167,7 @@ public void BuildWithMauiVersion(string id, string framework, string config, boo } string target = shouldPack ? "Pack" : ""; - Assert.True(DotnetInternal.Build(projectFile, config, target: target, binlogPath: binlogDir, properties: buildProps), + Assert.True(DotnetInternal.Build(projectFile, config, target: target, binlogPath: binlogDir, properties: buildProps, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } @@ -184,7 +184,7 @@ public void PreviousDotNetCanUseLatestMaui(string id, string config, bool should var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, DotNetPrevious), + Assert.True(DotnetInternal.New(id, projectDir, DotNetPrevious, output: _output), $"Unable to create template {id}. Check test output for errors."); if (shouldPack) @@ -204,7 +204,7 @@ public void PreviousDotNetCanUseLatestMaui(string id, string config, bool should """); string target = shouldPack ? "Pack" : ""; - Assert.True(DotnetInternal.Build(projectFile, config, target: target, properties: BuildProps), + Assert.True(DotnetInternal.Build(projectFile, config, target: target, properties: BuildProps, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } #endif @@ -215,12 +215,12 @@ public void BuildHandlesBadFilesInImages() var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New("maui", projectDir, DotNetCurrent), + Assert.True(DotnetInternal.New("maui", projectDir, DotNetCurrent, output: _output), $"Unable to create template maui. Check test output for errors."); File.WriteAllText(Path.Combine(projectDir, "Resources", "Images", ".DS_Store"), "Boom!"); - Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: BuildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: BuildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } @@ -237,7 +237,7 @@ public void PackCoreLib(string id, string framework, string config) var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, framework), + Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); var projectSectionsToReplace = new Dictionary() @@ -255,7 +255,7 @@ public void PackCoreLib(string id, string framework, string config) FileUtilities.ReplaceInFile(projectFile, projectSectionsToReplace); Directory.Delete(Path.Combine(projectDir, "Platforms"), recursive: true); - Assert.True(DotnetInternal.Build(projectFile, config, properties: BuildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(projectFile, config, properties: BuildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } @@ -268,7 +268,7 @@ public void BuildWithoutPackageReference(string id, string framework, string con var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, framework), + Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); FileUtilities.ReplaceInFile(projectFile, @@ -278,7 +278,7 @@ public void BuildWithoutPackageReference(string id, string framework, string con "", ""); - Assert.True(DotnetInternal.Build(projectFile, config, properties: BuildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(projectFile, config, properties: BuildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } @@ -294,7 +294,7 @@ public void BuildWithDifferentVersionNumber(string id, string config, string dis var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir), + Assert.True(DotnetInternal.New(id, projectDir, output: _output), $"Unable to create template {id}. Check test output for errors."); FileUtilities.ReplaceInFile(projectFile, @@ -311,7 +311,7 @@ public void BuildWithDifferentVersionNumber(string id, string config, string dis additionalDotNetBuildParams.Split(" ").ToList().ForEach(p => buildProps.Add(p)); } - Assert.True(DotnetInternal.Build(projectFile, config, properties: buildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(projectFile, config, properties: buildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } @@ -324,7 +324,7 @@ public void AspireServiceDefaultsTemplateUsesCorrectProjectName(string projectNa var projectDir = Path.Combine(TestDirectory, projectName); var expectedProjectFile = Path.Combine(projectDir, $"{projectName}.csproj"); - Assert.True(DotnetInternal.New("maui-aspire-servicedefaults", projectDir, additionalDotNetNewParams: $"-n \"{projectName}\""), + Assert.True(DotnetInternal.New("maui-aspire-servicedefaults", projectDir, additionalDotNetNewParams: $"-n \"{projectName}\"", output: _output), $"Unable to create template maui-aspire-servicedefaults. Check test output for errors."); // Verify the project file was created with the correct name (this was the bug) diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/BuildWarningsUtilities.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/BuildWarningsUtilities.cs index 9361fd3bfa9e..707bde08350f 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/BuildWarningsUtilities.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/BuildWarningsUtilities.cs @@ -1,6 +1,8 @@ -using System.Linq; +using System.IO; +using System.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Logging.StructuredLogger; +using Xunit.Abstractions; namespace Microsoft.Maui.IntegrationTests { @@ -25,6 +27,50 @@ public static class BuildWarningsUtilities private static string NormalizeFilePath(string file) => file.Replace("\\\\", "/", StringComparison.Ordinal).Replace('\\', '/'); + /// + /// Reads build errors from a binlog file and outputs them to the test output. + /// This makes errors visible in Azure DevOps logs instead of requiring artifact downloads. + /// + /// Path to the .binlog file + /// Maximum number of errors to output (default 50) + /// Optional test output helper for logging + public static void OutputBuildErrorsFromBinLog(string binLogFilePath, int maxErrors = 50, ITestOutputHelper? output = null) + { + if (!System.IO.File.Exists(binLogFilePath)) + { + output?.WriteLine($"[BuildWarningsUtilities] Binlog file not found: {binLogFilePath}"); + return; + } + + var errors = new List(); + foreach (var record in new BinLogReader().ReadRecords(binLogFilePath)) + { + if (record.Args is BuildErrorEventArgs error) + { + var file = NormalizeFilePath(error.File ?? ""); + var location = error.LineNumber > 0 ? $"({error.LineNumber},{error.ColumnNumber})" : ""; + errors.Add($"{file}{location}: error {error.Code}: {error.Message}"); + } + } + + if (errors.Count > 0) + { + output?.WriteLine(""); + output?.WriteLine("╔══════════════════════════════════════════════════════════════════════════════╗"); + output?.WriteLine($"║ BUILD ERRORS FROM BINLOG ({errors.Count} total)"); + output?.WriteLine("╚══════════════════════════════════════════════════════════════════════════════╝"); + foreach (var err in errors.Take(maxErrors)) + { + output?.WriteLine(err); + } + if (errors.Count > maxErrors) + { + output?.WriteLine($"... and {errors.Count - maxErrors} more errors (see binlog for full list)"); + } + output?.WriteLine(""); + } + } + public static List ReadNativeAOTWarningsFromBinLog(string binLogFilePath) { var actualWarnings = new List(); diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/DotnetInternal.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/DotnetInternal.cs index db9d30f2d6ff..4d41b1deebcf 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/DotnetInternal.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/DotnetInternal.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO; +using Xunit.Abstractions; namespace Microsoft.Maui.IntegrationTests { @@ -9,7 +10,7 @@ public static class DotnetInternal static readonly string DotnetTool = Path.Combine(DotnetRoot, "dotnet"); const int DEFAULT_TIMEOUT = 1800; - private static string ConstructBuildArgs(string projectFile, string config, string target = "", string framework = "", IEnumerable? properties = null, string binlogPath = "", string runtimeIdentifier = "", bool isPublishing = false) + private static (string buildArgs, string binlogPath) ConstructBuildArgs(string projectFile, string config, string target = "", string framework = "", IEnumerable? properties = null, string binlogPath = "", string runtimeIdentifier = "", bool isPublishing = false) { var buildArgs = $"\"{projectFile}\" -c {config}"; @@ -43,13 +44,13 @@ private static string ConstructBuildArgs(string projectFile, string config, stri } buildArgs += $" -bl:\"{binlogPath}\""; - return buildArgs; + return (buildArgs, binlogPath); } public static bool Build(string projectFile, string config, string target = "", string framework = "", IEnumerable? properties = null, string binlogPath = "", bool msbuildWarningsAsErrors = false, string runtimeIdentifier = "", - string[]? warningsToIgnore = null) + string[]? warningsToIgnore = null, ITestOutputHelper? output = null) { - var buildArgs = ConstructBuildArgs(projectFile, config, target, framework, properties, binlogPath, runtimeIdentifier, false); + var (buildArgs, actualBinlogPath) = ConstructBuildArgs(projectFile, config, target, framework, properties, binlogPath, runtimeIdentifier, false); if (msbuildWarningsAsErrors) { @@ -78,16 +79,32 @@ public static bool Build(string projectFile, string config, string target = "", buildArgs += $" -p:nowarn=\"{csWarnings}\""; } - return Run("build", $"{buildArgs}"); + var result = Run("build", $"{buildArgs}", output: output); + + // On failure, extract and output errors from the binlog for visibility in CI logs + if (!result) + { + BuildWarningsUtilities.OutputBuildErrorsFromBinLog(actualBinlogPath, output: output); + } + + return result; } - public static bool Publish(string projectFile, string config, string target = "", string framework = "", IEnumerable? properties = null, string binlogPath = "", string runtimeIdentifier = "") + public static bool Publish(string projectFile, string config, string target = "", string framework = "", IEnumerable? properties = null, string binlogPath = "", string runtimeIdentifier = "", ITestOutputHelper? output = null) { - var buildArgs = ConstructBuildArgs(projectFile, config, target, framework, properties, binlogPath, runtimeIdentifier, true); - return Run("publish", $"{buildArgs}"); + var (buildArgs, actualBinlogPath) = ConstructBuildArgs(projectFile, config, target, framework, properties, binlogPath, runtimeIdentifier, true); + var result = Run("publish", $"{buildArgs}", output: output); + + // On failure, extract and output errors from the binlog for visibility in CI logs + if (!result) + { + BuildWarningsUtilities.OutputBuildErrorsFromBinLog(actualBinlogPath, output: output); + } + + return result; } - public static bool New(string shortName, string outputDirectory, string framework = "", string? additionalDotNetNewParams = null) + public static bool New(string shortName, string outputDirectory, string framework = "", string? additionalDotNetNewParams = null, ITestOutputHelper? output = null) { var args = $"{shortName} -o \"{outputDirectory}\""; @@ -98,23 +115,23 @@ public static bool New(string shortName, string outputDirectory, string framewor args += $" {additionalDotNetNewParams}"; - var output = RunForOutput("new", args, out int exitCode, timeoutInSeconds: 300); - Console.WriteLine(output); + var cmdOutput = RunForOutput("new", args, out int exitCode, timeoutInSeconds: 300, output: output); + output?.WriteLine(cmdOutput); return exitCode == 0; } - public static bool Run(string command, string args, int timeoutinSeconds = DEFAULT_TIMEOUT) + public static bool Run(string command, string args, int timeoutinSeconds = DEFAULT_TIMEOUT, ITestOutputHelper? output = null) { - var runOutput = RunForOutput(command, args, out int exitCode, timeoutinSeconds); - Console.WriteLine($"Process exit code: {exitCode}"); - Console.WriteLine($"-------- Process output start --------"); - Console.WriteLine(runOutput); - Console.WriteLine($"-------- Process output end --------"); + var runOutput = RunForOutput(command, args, out int exitCode, timeoutinSeconds, output: output); + output?.WriteLine($"Process exit code: {exitCode}"); + output?.WriteLine($"-------- Process output start --------"); + output?.WriteLine(runOutput); + output?.WriteLine($"-------- Process output end --------"); // Provide helpful messages for common errors if (exitCode != 0) { - CheckForCommonErrors(runOutput); + CheckForCommonErrors(runOutput, output); } return exitCode == 0; @@ -123,43 +140,43 @@ public static bool Run(string command, string args, int timeoutinSeconds = DEFAU /// /// Checks build output for common errors and provides helpful guidance. /// - static void CheckForCommonErrors(string output) + static void CheckForCommonErrors(string cmdOutput, ITestOutputHelper? output) { // Check for Xcode version mismatch - if (output.Contains("requires Xcode", StringComparison.OrdinalIgnoreCase) && output.Contains("The current version of Xcode is", StringComparison.OrdinalIgnoreCase)) + if (cmdOutput.Contains("requires Xcode", StringComparison.OrdinalIgnoreCase) && cmdOutput.Contains("The current version of Xcode is", StringComparison.OrdinalIgnoreCase)) { // Extract the error message line for display - var errorLine = output.Split('\n') + var errorLine = cmdOutput.Split('\n') .FirstOrDefault(line => line.Contains("requires Xcode", StringComparison.OrdinalIgnoreCase)) ?.Trim() ?? "Xcode version mismatch"; - Console.WriteLine(""); - Console.WriteLine("╔══════════════════════════════════════════════════════════════════════════════╗"); - Console.WriteLine("║ XCODE VERSION MISMATCH DETECTED ║"); - Console.WriteLine("╠══════════════════════════════════════════════════════════════════════════════╣"); - Console.WriteLine($" {errorLine}"); - Console.WriteLine("╠══════════════════════════════════════════════════════════════════════════════╣"); - Console.WriteLine("║ To skip Xcode version validation, you can: ║"); - Console.WriteLine("║ ║"); - Console.WriteLine("║ 1. Set environment variable: ║"); - Console.WriteLine("║ export SKIP_XCODE_VERSION_CHECK=true ║"); - Console.WriteLine("║ ║"); - Console.WriteLine("║ 2. Or set SkipXcodeVersionCheck in TestEnvironment.cs ║"); - Console.WriteLine("║ src/TestUtils/src/.../Utilities/TestEnvironment.cs ║"); - Console.WriteLine("╚══════════════════════════════════════════════════════════════════════════════╝"); - Console.WriteLine(""); + output?.WriteLine(""); + output?.WriteLine("╔══════════════════════════════════════════════════════════════════════════════╗"); + output?.WriteLine("║ XCODE VERSION MISMATCH DETECTED ║"); + output?.WriteLine("╠══════════════════════════════════════════════════════════════════════════════╣"); + output?.WriteLine($" {errorLine}"); + output?.WriteLine("╠══════════════════════════════════════════════════════════════════════════════╣"); + output?.WriteLine("║ To skip Xcode version validation, you can: ║"); + output?.WriteLine("║ ║"); + output?.WriteLine("║ 1. Set environment variable: ║"); + output?.WriteLine("║ export SKIP_XCODE_VERSION_CHECK=true ║"); + output?.WriteLine("║ ║"); + output?.WriteLine("║ 2. Or set SkipXcodeVersionCheck in TestEnvironment.cs ║"); + output?.WriteLine("║ src/TestUtils/src/.../Utilities/TestEnvironment.cs ║"); + output?.WriteLine("╚══════════════════════════════════════════════════════════════════════════════╝"); + output?.WriteLine(""); } } - public static string RunForOutput(string command, string args, out int exitCode, int timeoutInSeconds = DEFAULT_TIMEOUT) + public static string RunForOutput(string command, string args, out int exitCode, int timeoutInSeconds = DEFAULT_TIMEOUT, ITestOutputHelper? output = null) { - Console.WriteLine($"Running: '{DotnetTool}' with '{command}'"); - Console.WriteLine($"Args list: {args}"); + output?.WriteLine($"Running: '{DotnetTool}' with '{command}'"); + output?.WriteLine($"Args list: {args}"); var pinfo = new ProcessStartInfo(DotnetTool, $"{command} {args}"); pinfo.EnvironmentVariables["DOTNET_MULTILEVEL_LOOKUP"] = "0"; pinfo.EnvironmentVariables["DOTNET_ROOT"] = DotnetRoot; - return ToolRunner.Run(pinfo, out exitCode, timeoutInSeconds: timeoutInSeconds); + return ToolRunner.Run(pinfo, out exitCode, timeoutInSeconds: timeoutInSeconds, output: output); } } diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/ToolRunner.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/ToolRunner.cs index 48358f066c2e..32509f4a7a10 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/ToolRunner.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/ToolRunner.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Text; +using Xunit.Abstractions; namespace Microsoft.Maui.IntegrationTests { @@ -7,24 +8,25 @@ public static class ToolRunner { public static string Run(string tool, string args, out int exitCode, string workingDirectory = "", - int timeoutInSeconds = 600) + int timeoutInSeconds = 600, + ITestOutputHelper? output = null) { var info = new ProcessStartInfo(tool, args); if (Directory.Exists(workingDirectory)) info.WorkingDirectory = workingDirectory; - return Run(info, out exitCode, timeoutInSeconds); + return Run(info, out exitCode, timeoutInSeconds, output: output); } public static string Run(ProcessStartInfo info, out int exitCode, - int timeoutInSeconds = 600, Action? inputAction = null) + int timeoutInSeconds = 600, Action? inputAction = null, ITestOutputHelper? output = null) { var procOutput = new StringBuilder(); using (var p = new Process()) { p.StartInfo = info; - Console.WriteLine($"[ToolRunner] Running: {p.StartInfo.FileName} {p.StartInfo.Arguments}"); + output?.WriteLine($"[ToolRunner] Running: {p.StartInfo.FileName} {p.StartInfo.Arguments}"); p.StartInfo.CreateNoWindow = true; p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; @@ -62,7 +64,7 @@ public static string Run(ProcessStartInfo info, out int exitCode, if (p.WaitForExit(timeoutInSeconds * 1000)) { exitCode = p.ExitCode; - Console.WriteLine($"[ToolRunner] Process '{Path.GetFileName(p.StartInfo.FileName)}' exited with code: {exitCode}"); + output?.WriteLine($"[ToolRunner] Process '{Path.GetFileName(p.StartInfo.FileName)}' exited with code: {exitCode}"); } else { diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/XHarness.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/XHarness.cs index 47e69166fb06..288393d0aabf 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/XHarness.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/Utilities/XHarness.cs @@ -1,15 +1,17 @@ -namespace Microsoft.Maui.IntegrationTests +using Xunit.Abstractions; + +namespace Microsoft.Maui.IntegrationTests { public static class XHarness { static readonly string XHarnessTool = "xharness"; const int DEFAULT_TIMEOUT = 300; - public static bool RunAndroid(string packageName, string resultDir, int expectedExitCode, int launchTimeoutSeconds = 120) + public static bool RunAndroid(string packageName, string resultDir, int expectedExitCode, int launchTimeoutSeconds = 120, ITestOutputHelper? output = null) { var timeoutString = TimeSpan.FromSeconds(launchTimeoutSeconds).ToString(); var args = $"android run --package-name={packageName} --output-directory=\"{resultDir}\" --expected-exit-code={expectedExitCode} --timeout=\"{timeoutString}\" --verbosity=Debug"; - return Run(args, launchTimeoutSeconds + 30); + return Run(args, launchTimeoutSeconds + 30, output: output); } /// @@ -25,8 +27,9 @@ public static bool RunAndroid(string packageName, string resultDir, int expected /// XHarness target device string (e.g., "ios-simulator-64_18.5") /// Optional specific device UDID to use /// How long to let the app run before killing it (default: 15s) + /// Optional test output helper for logging /// True if the app ran successfully (didn't crash), false otherwise - public static bool RunAppleForTimeout(string appPath, string resultDir, string targetDevice, string? deviceUdid = null, int launchTimeoutSeconds = 15) + public static bool RunAppleForTimeout(string appPath, string resultDir, string targetDevice, string? deviceUdid = null, int launchTimeoutSeconds = 15, ITestOutputHelper? output = null) { var timeoutString = TimeSpan.FromSeconds(launchTimeoutSeconds).ToString(); @@ -37,7 +40,7 @@ public static bool RunAppleForTimeout(string appPath, string resultDir, string t } var args = $"apple run --app=\"{appPath}\" --output-directory=\"{resultDir}\" {deviceArg} --timeout=\"{timeoutString}\" --verbosity=Debug"; - var xhOutput = RunForOutput(args, out int exitCode, launchTimeoutSeconds + 30); + var xhOutput = RunForOutput(args, out int exitCode, launchTimeoutSeconds + 30, output: output); // XHarness exit codes - see https://github.com/dotnet/xharness/blob/main/src/Microsoft.DotNet.XHarness.Common/CLI/ExitCode.cs // Success cases: @@ -55,38 +58,38 @@ public static bool RunAppleForTimeout(string appPath, string resultDir, string t if (!isSuccess) { - Console.WriteLine($"XHarness failed with exit code {exitCode}"); - Console.WriteLine(xhOutput); + output?.WriteLine($"XHarness failed with exit code {exitCode}"); + output?.WriteLine(xhOutput); } return isSuccess; } - public static bool InstallSimulator(string targetDevice) + public static bool InstallSimulator(string targetDevice, ITestOutputHelper? output = null) { - return Run($"apple simulators install \"{targetDevice}\" "); + return Run($"apple simulators install \"{targetDevice}\" ", output: output); } - public static string GetSimulatorUDID(string targetDevice) + public static string GetSimulatorUDID(string targetDevice, ITestOutputHelper? output = null) { var logDir = TestEnvironment.GetLogDirectory(); Directory.CreateDirectory(logDir); var diagnosticsPath = Path.Combine(logDir, $"xharness-device-{targetDevice.Replace("/", "-", StringComparison.Ordinal)}.log"); - return RunForOutput($"apple device \"{targetDevice}\" --diagnostics=\"{diagnosticsPath}\"", out _, timeoutInSeconds: 30); + return RunForOutput($"apple device \"{targetDevice}\" --diagnostics=\"{diagnosticsPath}\"", out _, timeoutInSeconds: 30, output: output); } - public static bool Run(string args, int timeoutInSeconds = DEFAULT_TIMEOUT) + public static bool Run(string args, int timeoutInSeconds = DEFAULT_TIMEOUT, ITestOutputHelper? output = null) { - var xhOutput = RunForOutput(args, out int exitCode, timeoutInSeconds); + var xhOutput = RunForOutput(args, out int exitCode, timeoutInSeconds, output: output); if (exitCode != 0) - Console.WriteLine(xhOutput); + output?.WriteLine(xhOutput); return exitCode == 0; } - public static string RunForOutput(string args, out int exitCode, int timeoutInSeconds = DEFAULT_TIMEOUT) + public static string RunForOutput(string args, out int exitCode, int timeoutInSeconds = DEFAULT_TIMEOUT, ITestOutputHelper? output = null) { - return DotnetInternal.RunForOutput(XHarnessTool, args, out exitCode, timeoutInSeconds); + return DotnetInternal.RunForOutput(XHarnessTool, args, out exitCode, timeoutInSeconds, output: output); } } } \ No newline at end of file diff --git a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/WindowsTemplateTest.cs b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/WindowsTemplateTest.cs index d20e753830c6..ea4c55cce73e 100644 --- a/src/TestUtils/src/Microsoft.Maui.IntegrationTests/WindowsTemplateTest.cs +++ b/src/TestUtils/src/Microsoft.Maui.IntegrationTests/WindowsTemplateTest.cs @@ -20,7 +20,7 @@ public void BuildPackaged(string id, string framework, string config) var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, framework), + Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); // .NET 9 and later was Unpackaged, so we need to remove the line @@ -28,7 +28,7 @@ public void BuildPackaged(string id, string framework, string config) "None", ""); - Assert.True(DotnetInternal.Build(projectFile, config, properties: BuildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(projectFile, config, properties: BuildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } @@ -52,7 +52,7 @@ public void BuildWindowsAppSDKSelfContained(string id, bool wasdkself, bool nets var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent), + Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent, output: _output), $"Unable to create template {id}. Check test output for errors."); FileUtilities.ReplaceInFile(projectFile, @@ -66,7 +66,7 @@ public void BuildWindowsAppSDKSelfContained(string id, bool wasdkself, bool nets var extendedBuildProps = BuildProps; extendedBuildProps.Add($"TargetFramework={DotNetCurrent}-windows10.0.19041.0"); - Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: extendedBuildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: extendedBuildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } @@ -86,7 +86,7 @@ public void BuildWindowsRidGraph(string id, bool useRidGraph, string packageType var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent), + Assert.True(DotnetInternal.New(id, projectDir, DotNetCurrent, output: _output), $"Unable to create template {id}. Check test output for errors."); FileUtilities.ReplaceInFile(projectFile, @@ -99,7 +99,7 @@ public void BuildWindowsRidGraph(string id, bool useRidGraph, string packageType var extendedBuildProps = BuildProps; extendedBuildProps.Add($"TargetFramework={DotNetCurrent}-windows10.0.19041.0"); - Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: extendedBuildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: extendedBuildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } @@ -119,14 +119,14 @@ public void PublishUnpackaged(string id, string framework, string config, bool u var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, framework), + Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); // .NET 9 is Unpackaged by default, so we don't have to do anything FileUtilities.ShouldContainInFile(projectFile, "None"); - Assert.True(DotnetInternal.Publish(projectFile, config, framework: $"{framework}-windows10.0.19041.0", properties: BuildProps), + Assert.True(DotnetInternal.Publish(projectFile, config, framework: $"{framework}-windows10.0.19041.0", properties: BuildProps, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); var rid = usesRidGraph ? "win10-x64" : "win-x64"; @@ -163,7 +163,7 @@ public void PublishPackaged(string id, string framework, string config, bool use var name = Path.GetFileName(projectDir); var projectFile = Path.Combine(projectDir, $"{name}.csproj"); - Assert.True(DotnetInternal.New(id, projectDir, framework), + Assert.True(DotnetInternal.New(id, projectDir, framework, output: _output), $"Unable to create template {id}. Check test output for errors."); // .NET 9 and later was Unpackaged, so we need to remove the line @@ -171,7 +171,7 @@ public void PublishPackaged(string id, string framework, string config, bool use "None", ""); - Assert.True(DotnetInternal.Publish(projectFile, config, framework: $"{framework}-windows10.0.19041.0", properties: BuildProps), + Assert.True(DotnetInternal.Publish(projectFile, config, framework: $"{framework}-windows10.0.19041.0", properties: BuildProps, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); var rid = usesRidGraph ? "win10-x64/" : ""; @@ -196,7 +196,7 @@ public void BuildWithIdentityClient() var projectDir = TestDirectory; var projectFile = Path.Combine(projectDir, $"{Path.GetFileName(projectDir)}.csproj"); - Assert.True(DotnetInternal.New("maui", projectDir, DotNetCurrent), + Assert.True(DotnetInternal.New("maui", projectDir, DotNetCurrent, output: _output), $"Unable to create template maui. Check test output for errors."); // .NET 9 and later was Unpackaged, so we need to remove the line @@ -210,7 +210,7 @@ public void BuildWithIdentityClient() """); - Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: BuildProps, msbuildWarningsAsErrors: true), + Assert.True(DotnetInternal.Build(projectFile, "Debug", properties: BuildProps, msbuildWarningsAsErrors: true, output: _output), $"Project {Path.GetFileName(projectFile)} failed to build. Check test output/attachments for errors."); } }