From eb53852d7ae6838e54525eb57df1d8ce8a722f9b Mon Sep 17 00:00:00 2001 From: Sean Hall Date: Fri, 24 Jun 2022 12:28:27 -0500 Subject: Add longPathAware to Burn manifest to support long paths. Fixes 3455 --- src/test/burn/Directory.wixproj.targets | 1 + .../NonCompressedBundle.wixproj | 19 ++ .../NonCompressedBundle/NonCompressedBundle.wxs | 10 + .../LongPathTests/PackageA/PackageA.wixproj | 10 + src/test/burn/TestData/Templates/Bundle.wxs | 5 +- src/test/burn/TestExe/Task.cs | 67 +++++ src/test/burn/WixTestTools/BundleInstaller.cs | 4 +- src/test/burn/WixTestTools/MSIExec.cs | 14 +- src/test/burn/WixTestTools/TestTool.cs | 2 +- .../burn/WixToolsetTest.BurnE2E/LongPathTests.cs | 298 +++++++++++++++++++++ .../testhost.longpathaware.manifest | 11 + src/test/burn/test_burn.cmd | 5 + 12 files changed, 439 insertions(+), 7 deletions(-) create mode 100644 src/test/burn/TestData/LongPathTests/NonCompressedBundle/NonCompressedBundle.wixproj create mode 100644 src/test/burn/TestData/LongPathTests/NonCompressedBundle/NonCompressedBundle.wxs create mode 100644 src/test/burn/TestData/LongPathTests/PackageA/PackageA.wixproj create mode 100644 src/test/burn/WixToolsetTest.BurnE2E/LongPathTests.cs create mode 100644 src/test/burn/WixToolsetTest.BurnE2E/testhost.longpathaware.manifest (limited to 'src/test') diff --git a/src/test/burn/Directory.wixproj.targets b/src/test/burn/Directory.wixproj.targets index 17a46e2a..4037e865 100644 --- a/src/test/burn/Directory.wixproj.targets +++ b/src/test/burn/Directory.wixproj.targets @@ -7,6 +7,7 @@ http://localhost:9999/e2e/ TestGroupName=$(TestGroupName);PackageName=$(PackageName);BundleName=$(BundleName);WebServerBaseUrl=$(WebServerBaseUrl);$(DefineConstants) BA=$(BA);$(DefineConstants) + BundleLogDirectory=$(BundleLogDirectory);$(DefineConstants) CabPrefix=$(CabPrefix);$(DefineConstants) SoftwareTag=1;$(DefineConstants) ProductCode=$(ProductCode);$(DefineConstants) diff --git a/src/test/burn/TestData/LongPathTests/NonCompressedBundle/NonCompressedBundle.wixproj b/src/test/burn/TestData/LongPathTests/NonCompressedBundle/NonCompressedBundle.wixproj new file mode 100644 index 00000000..3686200c --- /dev/null +++ b/src/test/burn/TestData/LongPathTests/NonCompressedBundle/NonCompressedBundle.wixproj @@ -0,0 +1,19 @@ + + + + Bundle + {EFCF768F-1B06-4B68-9DE0-9244F8212D31} + [LocalAppDataFolder]Temp + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/burn/TestData/LongPathTests/NonCompressedBundle/NonCompressedBundle.wxs b/src/test/burn/TestData/LongPathTests/NonCompressedBundle/NonCompressedBundle.wxs new file mode 100644 index 00000000..e3872eb1 --- /dev/null +++ b/src/test/burn/TestData/LongPathTests/NonCompressedBundle/NonCompressedBundle.wxs @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/test/burn/TestData/LongPathTests/PackageA/PackageA.wixproj b/src/test/burn/TestData/LongPathTests/PackageA/PackageA.wixproj new file mode 100644 index 00000000..798452e7 --- /dev/null +++ b/src/test/burn/TestData/LongPathTests/PackageA/PackageA.wixproj @@ -0,0 +1,10 @@ + + + + a + {4DC05A2A-382D-4E7D-B6DA-163908396373} + + + + + \ No newline at end of file diff --git a/src/test/burn/TestData/Templates/Bundle.wxs b/src/test/burn/TestData/Templates/Bundle.wxs index b211d9c3..612e67f5 100644 --- a/src/test/burn/TestData/Templates/Bundle.wxs +++ b/src/test/burn/TestData/Templates/Bundle.wxs @@ -3,10 +3,13 @@ + + + - + diff --git a/src/test/burn/TestExe/Task.cs b/src/test/burn/TestExe/Task.cs index 59f774fb..0d283c6c 100644 --- a/src/test/burn/TestExe/Task.cs +++ b/src/test/burn/TestExe/Task.cs @@ -2,8 +2,10 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using Microsoft.Win32; namespace TestExe @@ -151,6 +153,67 @@ namespace TestExe } } + public class DeleteManifestsTask : Task + { + public DeleteManifestsTask(string Data) : base(Data) { } + + public override void RunTask() + { + string filePath = System.Environment.ExpandEnvironmentVariables(this.data); + IntPtr type = new IntPtr(24); //RT_MANIFEST + IntPtr name = new IntPtr(1); //CREATEPROCESS_MANIFEST_RESOURCE_ID + DeleteResource(filePath, type, name, 1033); + } + + private static void DeleteResource(string filePath, IntPtr type, IntPtr name, ushort language, bool throwOnError = false) + { + bool discard = true; + IntPtr handle = BeginUpdateResourceW(filePath, false); + try + { + if (handle == IntPtr.Zero) + { + throw new Win32Exception(); + } + + if (!UpdateResourceW(handle, type, name, language, IntPtr.Zero, 0)) + { + throw new Win32Exception(); + } + + discard = false; + } + catch + { + if (throwOnError) + { + throw; + } + } + finally + { + if (handle != IntPtr.Zero) + { + if (!EndUpdateResourceW(handle, discard) && throwOnError) + { + throw new Win32Exception(); + } + } + } + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + private extern static IntPtr BeginUpdateResourceW(string fileName, [MarshalAs(UnmanagedType.Bool)] bool deleteExistingResources); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private extern static bool UpdateResourceW(IntPtr hUpdate, IntPtr type, IntPtr name, ushort language, IntPtr pData, uint cb); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private extern static bool EndUpdateResourceW(IntPtr hUpdate, [MarshalAs(UnmanagedType.Bool)] bool discard); + } + public class TaskParser { @@ -197,6 +260,10 @@ namespace TestExe t = new FileExistsTask(args[i + 1]); tasks.Add(t); break; + case "/dm": + t = new DeleteManifestsTask(args[i + 1]); + tasks.Add(t); + break; #if NET35 case "/pinfo": t = new ProcessInfoTask(args[i + 1]); diff --git a/src/test/burn/WixTestTools/BundleInstaller.cs b/src/test/burn/WixTestTools/BundleInstaller.cs index 5551d3c0..0f2cfa8f 100644 --- a/src/test/burn/WixTestTools/BundleInstaller.cs +++ b/src/test/burn/WixTestTools/BundleInstaller.cs @@ -28,6 +28,8 @@ namespace WixTestTools public int? AlternateExitCode { get; set; } + public string LogDirectory { get; set; } + public int? LastExitCode { get; set; } /// @@ -194,7 +196,7 @@ namespace WixTestTools sb.Append(" -quiet"); // Generate the log file name. - string logFile = Path.Combine(Path.GetTempPath(), String.Format("{0}_{1}_{2:yyyyMMddhhmmss}_{4}_{3}.log", this.TestGroupName, this.TestName, DateTime.UtcNow, Path.GetFileNameWithoutExtension(this.Bundle), mode)); + string logFile = Path.Combine(this.LogDirectory ?? Path.GetTempPath(), String.Format("{0}_{1}_{2:yyyyMMddhhmmss}_{4}_{3}.log", this.TestGroupName, this.TestName, DateTime.UtcNow, Path.GetFileNameWithoutExtension(this.Bundle), mode)); sb.AppendFormat(" -log \"{0}\"", logFile); // Set operation. diff --git a/src/test/burn/WixTestTools/MSIExec.cs b/src/test/burn/WixTestTools/MSIExec.cs index a10a48d6..5f57da7b 100644 --- a/src/test/burn/WixTestTools/MSIExec.cs +++ b/src/test/burn/WixTestTools/MSIExec.cs @@ -110,7 +110,7 @@ namespace WixTestTools this.NoRestart = true; this.ForceRestart = false; this.PromptRestart = false; - this.LogFile = string.Empty; + this.LogFile = String.Empty; this.LoggingOptions = MSIExecLoggingOptions.VOICEWARMUP; this.OtherArguments = String.Empty; } @@ -230,14 +230,14 @@ namespace WixTestTools } // logfile and logging options - if (0 != loggingOptionsString.Length || !string.IsNullOrEmpty(this.LogFile)) + if (0 != loggingOptionsString.Length || !String.IsNullOrEmpty(this.LogFile)) { arguments.Append(" /l"); if (0 != loggingOptionsString.Length) { arguments.AppendFormat("{0} ", loggingOptionsString); } - if (!string.IsNullOrEmpty(this.LogFile)) + if (!String.IsNullOrEmpty(this.LogFile)) { arguments.AppendFormat(" \"{0}\" ", this.LogFile); } @@ -268,7 +268,7 @@ namespace WixTestTools }; // product - if (!string.IsNullOrEmpty(this.Product)) + if (!String.IsNullOrEmpty(this.Product)) { arguments.AppendFormat(" \"{0}\" ", this.Product); } @@ -310,6 +310,12 @@ namespace WixTestTools /// ERROR_CALL_NOT_IMPLEMENTED = 120, + /// + /// ERROR_FILENAME_EXCED_RANGE 206 + /// The filename or extension is too long. + /// + ERROR_FILENAME_EXCED_RANGE = 206, + /// /// ERROR_APPHELP_BLOCK 1259 /// If Windows Installer determines a product may be incompatible with the current operating system, diff --git a/src/test/burn/WixTestTools/TestTool.cs b/src/test/burn/WixTestTools/TestTool.cs index eb77c75b..79e7004c 100644 --- a/src/test/burn/WixTestTools/TestTool.cs +++ b/src/test/burn/WixTestTools/TestTool.cs @@ -229,7 +229,7 @@ namespace WixTestTools returnValue.AppendLine("Tool run result:"); returnValue.AppendLine("----------------"); returnValue.AppendLine("Command:"); - returnValue.AppendLine($"\"{result.StartInfo.FileName}\" {result.StartInfo.Arguments}"); + returnValue.AppendLine($"\"{result.FileName}\" {result.Arguments}"); returnValue.AppendLine(); returnValue.AppendLine("Standard Output:"); foreach (var line in result.StandardOutput ?? new string[0]) diff --git a/src/test/burn/WixToolsetTest.BurnE2E/LongPathTests.cs b/src/test/burn/WixToolsetTest.BurnE2E/LongPathTests.cs new file mode 100644 index 00000000..ba793d7a --- /dev/null +++ b/src/test/burn/WixToolsetTest.BurnE2E/LongPathTests.cs @@ -0,0 +1,298 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.BurnE2E +{ + using System; + using System.ComponentModel; + using System.IO; + using System.Runtime.InteropServices; + using Microsoft.Win32; + using WixBuildTools.TestSupport; + using WixTestTools; + using WixToolset.Mba.Core; + using Xunit; + using Xunit.Abstractions; + + public class LongPathTests : BurnE2ETests + { + public LongPathTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } + + [RuntimeFact] + public void CanInstallAndUninstallSimpleBundle_x86_wixstdba() + { + this.CanInstallAndUninstallSimpleBundle("PackageA", "BundleA"); + } + + [RuntimeFact] + public void CanInstallAndUninstallSimpleBundle_x86_testba() + { + this.CanInstallAndUninstallSimpleBundle("PackageA", "BundleB"); + } + + [RuntimeFact] + public void CanInstallAndUninstallSimpleBundle_x86_dnctestba() + { + this.CanInstallAndUninstallSimpleBundle("PackageA", "BundleC"); + } + + [RuntimeFact] + public void CanInstallAndUninstallSimpleBundle_x86_wixba() + { + this.CanInstallAndUninstallSimpleBundle("PackageA", "BundleD"); + } + + [RuntimeFact] + public void CanInstallAndUninstallSimpleBundle_x64_wixstdba() + { + this.CanInstallAndUninstallSimpleBundle("PackageA_x64", "BundleA_x64"); + } + + [RuntimeFact] + public void CanInstallAndUninstallSimplePerUserBundle_x64_wixstdba() + { + this.CanInstallAndUninstallSimpleBundle("PackageApu_x64", "BundleApu_x64", "PackagePerUser.wxs", unchecked((int)0xc0000005)); + } + + [RuntimeFact] + public void CanInstallAndUninstallSimpleBundle_x64_testba() + { + this.CanInstallAndUninstallSimpleBundle("PackageA_x64", "BundleB_x64"); + } + + [RuntimeFact] + public void CanInstallAndUninstallSimpleBundle_x64_dnctestba() + { + this.CanInstallAndUninstallSimpleBundle("PackageA_x64", "BundleC_x64"); + } + + [RuntimeFact] + public void CanInstallAndUninstallSimpleBundle_x64_dncwixba() + { + this.CanInstallAndUninstallSimpleBundle("PackageA_x64", "BundleD_x64"); + } + + private void CanInstallAndUninstallSimpleBundle(string packageName, string bundleName, string fileName = "Package.wxs", int? alternateExitCode = null) + { + var package = this.CreatePackageInstaller(Path.Combine("..", "BasicFunctionalityTests", packageName)); + + var bundle = this.CreateBundleInstaller(Path.Combine("..", "BasicFunctionalityTests", bundleName)); + bundle.AlternateExitCode = alternateExitCode; + + using var dfs = new DisposableFileSystem(); + var baseFolder = GetLongPath(dfs.GetFolder()); + + var packageSourceCodeInstalled = package.GetInstalledFilePath(fileName); + + // Source file should *not* be installed + Assert.False(File.Exists(packageSourceCodeInstalled), $"{packageName} payload should not be there on test start: {packageSourceCodeInstalled}"); + + var bundleFileInfo = new FileInfo(bundle.Bundle); + var bundleCopiedPath = Path.Combine(baseFolder, bundleFileInfo.Name); + bundleFileInfo.CopyTo(bundleCopiedPath); + + bundle.Install(bundleCopiedPath); + bundle.VerifyRegisteredAndInPackageCache(); + + // Source file should be installed + Assert.True(File.Exists(packageSourceCodeInstalled), $"Should have found {packageName} payload installed at: {packageSourceCodeInstalled}"); + + if (alternateExitCode == bundle.LastExitCode) + { + WixAssert.Skip($"Install exited with {bundle.LastExitCode}"); + } + + bundle.Uninstall(bundleCopiedPath); + + // Source file should *not* be installed + Assert.False(File.Exists(packageSourceCodeInstalled), $"{packageName} payload should have been removed by uninstall from: {packageSourceCodeInstalled}"); + + bundle.VerifyUnregisteredAndRemovedFromPackageCache(); + + if (alternateExitCode == bundle.LastExitCode) + { + WixAssert.Skip($"Uninstall exited with {bundle.LastExitCode}"); + } + } + + [RuntimeFact] + public void CanLayoutNonCompressedBundleToLongPath() + { + var nonCompressedBundle = this.CreateBundleInstaller("NonCompressedBundle"); + var testBAController = this.CreateTestBAController(); + + testBAController.SetPackageRequestedState("NetFx48Web", RequestState.None); + + using var dfs = new DisposableFileSystem(); + var layoutDirectory = GetLongPath(dfs.GetFolder()); + + nonCompressedBundle.Layout(layoutDirectory); + nonCompressedBundle.VerifyUnregisteredAndRemovedFromPackageCache(); + + Assert.True(File.Exists(Path.Combine(layoutDirectory, "NonCompressedBundle.exe"))); + Assert.True(File.Exists(Path.Combine(layoutDirectory, "PackageA.msi"))); + Assert.True(File.Exists(Path.Combine(layoutDirectory, "1a.cab"))); + Assert.False(File.Exists(Path.Combine(layoutDirectory, @"redist\ndp48-web.exe"))); + } + + [RuntimeFact] + public void CanInstallNonCompressedBundleWithLongTempPath() + { + this.InstallNonCompressedBundle(longTemp: true, useOriginalTempForLog: true); + } + + [RuntimeFact] + public void CannotInstallNonCompressedBundleWithLongPackageCachePath() + { + var installLogPath = this.InstallNonCompressedBundle((int)MSIExec.MSIExecReturnCode.ERROR_INSTALL_FAILURE, longPackageCache: true); + Assert.True(LogVerifier.MessageInLogFile(installLogPath, @"Error 0x80070643: Failed to install MSI package")); + } + + [RuntimeFact] + public void CannotInstallNonCompressedBundleWithLongWorkingPath() + { + var installLogPath = this.InstallNonCompressedBundle((int)MSIExec.MSIExecReturnCode.ERROR_FILENAME_EXCED_RANGE | unchecked((int)0x80070000), longWorkingPath: true); + Assert.True(LogVerifier.MessageInLogFile(installLogPath, @"Error 0x800700ce: Failed to load BA DLL")); + } + + public string InstallNonCompressedBundle(int expectedExitCode = 0, bool longTemp = false, bool useOriginalTempForLog = false, bool longWorkingPath = false, bool longPackageCache = false, int? alternateExitCode = null) + { + var deletePolicyKey = false; + string originalEngineWorkingDirectoryValue = null; + string originalPackageCacheValue = null; + var originalTemp = Environment.GetEnvironmentVariable("TMP"); + var packageA = this.CreatePackageInstaller("PackageA"); + var nonCompressedBundle = this.CreateBundleInstaller("NonCompressedBundle"); + var policyPath = nonCompressedBundle.GetFullBurnPolicyRegistryPath(); + string installLogPath = null; + + try + { + using var dfs = new DisposableFileSystem(); + var originalBaseFolder = dfs.GetFolder(); + var baseFolder = GetLongPath(originalBaseFolder); + var sourceFolder = Path.Combine(baseFolder, "source"); + var workingFolder = Path.Combine(baseFolder, "working"); + var tempFolder = Path.Combine(originalBaseFolder, new string('d', 260 - originalBaseFolder.Length - 2)); + var packageCacheFolder = Path.Combine(baseFolder, "package cache"); + + var copyResult = TestDataFolderFileSystem.RobocopyFolder(this.TestContext.TestDataFolder, sourceFolder); + Assert.True(copyResult.ExitCode >= 0 && copyResult.ExitCode < 8, $"Exit code: {copyResult.ExitCode}\r\nOutput: {String.Join("\r\n", copyResult.StandardOutput)}\r\nError: {String.Join("\r\n", copyResult.StandardError)}"); + + var bundleFileInfo = new FileInfo(nonCompressedBundle.Bundle); + var bundleCopiedPath = Path.Combine(sourceFolder, bundleFileInfo.Name); + + var policyKey = Registry.LocalMachine.OpenSubKey(policyPath, writable: true); + if (policyKey == null) + { + policyKey = Registry.LocalMachine.CreateSubKey(policyPath, writable: true); + deletePolicyKey = true; + } + + using (policyKey) + { + originalEngineWorkingDirectoryValue = policyKey.GetValue("EngineWorkingDirectory") as string; + originalPackageCacheValue = policyKey.GetValue("PackageCache") as string; + + if (longWorkingPath) + { + policyKey.SetValue("EngineWorkingDirectory", workingFolder); + } + + if (longPackageCache) + { + policyKey.SetValue("PackageCache", packageCacheFolder); + } + } + + if (longTemp) + { + Environment.SetEnvironmentVariable("TMP", tempFolder); + + if (useOriginalTempForLog) + { + nonCompressedBundle.LogDirectory = originalTemp; + } + } + + try + { + nonCompressedBundle.AlternateExitCode = alternateExitCode; + installLogPath = nonCompressedBundle.Install(bundleCopiedPath, expectedExitCode); + + if (alternateExitCode == nonCompressedBundle.LastExitCode) + { + WixAssert.Skip($"Install exited with {nonCompressedBundle.LastExitCode}"); + } + } + finally + { + TestDataFolderFileSystem.RobocopyFolder(tempFolder, originalTemp); + } + + installLogPath = Path.Combine(originalTemp, Path.GetFileName(installLogPath)); + + if (expectedExitCode == 0) + { + var registration = nonCompressedBundle.VerifyRegisteredAndInPackageCache(); + packageA.VerifyInstalled(true); + + nonCompressedBundle.Uninstall(registration.CachePath); + + if (alternateExitCode == nonCompressedBundle.LastExitCode) + { + WixAssert.Skip($"Uninstall exited with {nonCompressedBundle.LastExitCode}"); + } + } + + nonCompressedBundle.VerifyUnregisteredAndRemovedFromPackageCache(); + packageA.VerifyInstalled(false); + + return installLogPath; + } + finally + { + Environment.SetEnvironmentVariable("TMP", originalTemp); + + if (deletePolicyKey) + { + Registry.LocalMachine.DeleteSubKeyTree(policyPath); + } + else + { + using (var policyKey = Registry.LocalMachine.OpenSubKey(policyPath, writable: true)) + { + policyKey?.SetValue("EngineWorkingDirectory", originalEngineWorkingDirectoryValue); + policyKey?.SetValue("PackageCache", originalPackageCacheValue); + } + } + } + } + + private static string GetLongPath(string baseFolder) + { + Directory.CreateDirectory(baseFolder); + + // Try to create a directory that is longer than MAX_PATH but without the \\?\ prefix to detect OS support for long paths. + // Need to PInvoke CreateDirectoryW directly because .NET methods will append the \\?\ prefix. + foreach (var c in new char[] { 'a', 'b', 'c' }) + { + baseFolder = Path.Combine(baseFolder, new string(c, 100)); + if (!CreateDirectoryW(baseFolder, IntPtr.Zero)) + { + int lastError = Marshal.GetLastWin32Error(); + if (lastError == 206) + { + WixAssert.Skip($"MAX_PATH is being enforced ({baseFolder})"); + } + throw new Win32Exception(lastError); + } + } + + return baseFolder; + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private extern static bool CreateDirectoryW(string lpPathName, IntPtr lpSecurityAttributes); + } +} diff --git a/src/test/burn/WixToolsetTest.BurnE2E/testhost.longpathaware.manifest b/src/test/burn/WixToolsetTest.BurnE2E/testhost.longpathaware.manifest new file mode 100644 index 00000000..a56ab91a --- /dev/null +++ b/src/test/burn/WixToolsetTest.BurnE2E/testhost.longpathaware.manifest @@ -0,0 +1,11 @@ + + + + + + + + true + + + diff --git a/src/test/burn/test_burn.cmd b/src/test/burn/test_burn.cmd index 83401614..80467f39 100644 --- a/src/test/burn/test_burn.cmd +++ b/src/test/burn/test_burn.cmd @@ -9,11 +9,16 @@ @if /i "%1"=="test" set RuntimeTestsEnabled=true @if not "%1"=="" shift & goto parse_args +@set _B=%~dp0..\..\..\build\IntegrationBurn\%_C% + @echo Burn integration tests %_C% msbuild -t:Build -Restore -p:Configuration=%_C% -warnaserror -bl:%_L%\test_burn_build.binlog || exit /b msbuild -t:Build -Restore TestData\TestData.proj -p:Configuration=%_C% -m -bl:%_L%\test_burn_data_build.binlog || exit /b +"%_B%\net35\win-x86\testexe.exe" /dm "%_B%\netcoreapp3.1\testhost.exe" +mt.exe -manifest "WixToolsetTest.BurnE2E\testhost.longpathaware.manifest" -updateresource:"%_B%\netcoreapp3.1\testhost.exe" + @if not "%RuntimeTestsEnabled%"=="true" goto :LExit dotnet test -c %_C% --no-build WixToolsetTest.BurnE2E -l "trx;LogFileName=%_L%\TestResults\WixToolsetTest.BurnE2E.trx" || exit /b -- cgit v1.2.3-55-g6feb