From badde27bf66fcacef2d625af0bd7590ecdc5804f Mon Sep 17 00:00:00 2001 From: Sean Hall Date: Wed, 17 Feb 2021 15:45:19 -0600 Subject: Create WixTestTools project and reorganize files. --- src/TestBA/Hresult.cs | 22 + src/TestBA/TestBA.BootstrapperCore.config | 18 + src/TestBA/TestBA.cs | 584 ++++++++++++++++ src/TestBA/TestBA.csproj | 24 + src/TestBA/TestBAFactory.cs | 22 + .../TestBA/TestBAWixlib/testbawixlib.wixproj | 2 +- src/Utilities/TestBA/Hresult.cs | 22 - .../TestBA/TestBA.BootstrapperCore.config | 18 - src/Utilities/TestBA/TestBA.cs | 584 ---------------- src/Utilities/TestBA/TestBA.csproj | 24 - src/Utilities/TestBA/TestBAFactory.cs | 22 - src/WixTestTools/BundleInstaller.cs | 141 ++++ src/WixTestTools/BundleRegistration.cs | 145 ++++ src/WixTestTools/BundleVerifier.cs | 96 +++ src/WixTestTools/MSIExec.cs | 753 +++++++++++++++++++++ src/WixTestTools/MsiUtilities.cs | 27 + src/WixTestTools/PackageInstaller.cs | 90 +++ src/WixTestTools/PackageVerifier.cs | 77 +++ src/WixTestTools/TestTool.cs | 245 +++++++ src/WixTestTools/WixTestBase.cs | 19 + src/WixTestTools/WixTestContext.cs | 73 ++ src/WixTestTools/WixTestTools.csproj | 21 + src/WixToolsetTest.BurnE2E/BundleInstaller.cs | 141 ---- src/WixToolsetTest.BurnE2E/BundleRegistration.cs | 145 ---- src/WixToolsetTest.BurnE2E/BundleVerifier.cs | 96 --- src/WixToolsetTest.BurnE2E/BurnE2ETests.cs | 1 + src/WixToolsetTest.BurnE2E/FailureTests.cs | 1 + src/WixToolsetTest.BurnE2E/MSIExec.cs | 753 --------------------- src/WixToolsetTest.BurnE2E/MsiTransactionTests.cs | 1 + src/WixToolsetTest.BurnE2E/MsiUtilities.cs | 27 - src/WixToolsetTest.BurnE2E/PackageInstaller.cs | 90 --- src/WixToolsetTest.BurnE2E/PackageVerifier.cs | 77 --- src/WixToolsetTest.BurnE2E/TestBAController.cs | 1 + src/WixToolsetTest.BurnE2E/TestTool.cs | 245 ------- src/WixToolsetTest.BurnE2E/WixTestBase.cs | 19 - src/WixToolsetTest.BurnE2E/WixTestContext.cs | 73 -- .../WixToolsetTest.BurnE2E.csproj | 4 + 37 files changed, 2366 insertions(+), 2337 deletions(-) create mode 100644 src/TestBA/Hresult.cs create mode 100644 src/TestBA/TestBA.BootstrapperCore.config create mode 100644 src/TestBA/TestBA.cs create mode 100644 src/TestBA/TestBA.csproj create mode 100644 src/TestBA/TestBAFactory.cs delete mode 100644 src/Utilities/TestBA/Hresult.cs delete mode 100644 src/Utilities/TestBA/TestBA.BootstrapperCore.config delete mode 100644 src/Utilities/TestBA/TestBA.cs delete mode 100644 src/Utilities/TestBA/TestBA.csproj delete mode 100644 src/Utilities/TestBA/TestBAFactory.cs create mode 100644 src/WixTestTools/BundleInstaller.cs create mode 100644 src/WixTestTools/BundleRegistration.cs create mode 100644 src/WixTestTools/BundleVerifier.cs create mode 100644 src/WixTestTools/MSIExec.cs create mode 100644 src/WixTestTools/MsiUtilities.cs create mode 100644 src/WixTestTools/PackageInstaller.cs create mode 100644 src/WixTestTools/PackageVerifier.cs create mode 100644 src/WixTestTools/TestTool.cs create mode 100644 src/WixTestTools/WixTestBase.cs create mode 100644 src/WixTestTools/WixTestContext.cs create mode 100644 src/WixTestTools/WixTestTools.csproj delete mode 100644 src/WixToolsetTest.BurnE2E/BundleInstaller.cs delete mode 100644 src/WixToolsetTest.BurnE2E/BundleRegistration.cs delete mode 100644 src/WixToolsetTest.BurnE2E/BundleVerifier.cs delete mode 100644 src/WixToolsetTest.BurnE2E/MSIExec.cs delete mode 100644 src/WixToolsetTest.BurnE2E/MsiUtilities.cs delete mode 100644 src/WixToolsetTest.BurnE2E/PackageInstaller.cs delete mode 100644 src/WixToolsetTest.BurnE2E/PackageVerifier.cs delete mode 100644 src/WixToolsetTest.BurnE2E/TestTool.cs delete mode 100644 src/WixToolsetTest.BurnE2E/WixTestBase.cs delete mode 100644 src/WixToolsetTest.BurnE2E/WixTestContext.cs (limited to 'src') diff --git a/src/TestBA/Hresult.cs b/src/TestBA/Hresult.cs new file mode 100644 index 00000000..bc1aa8c0 --- /dev/null +++ b/src/TestBA/Hresult.cs @@ -0,0 +1,22 @@ +// 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 WixToolset.Test.BA +{ + using System; + + /// + /// Utility class to work with HRESULTs + /// + internal class Hresult + { + /// + /// Determines if an HRESULT was a success code or not. + /// + /// HRESULT to verify. + /// True if the status is a success code. + public static bool Succeeded(int status) + { + return status >= 0; + } + } +} diff --git a/src/TestBA/TestBA.BootstrapperCore.config b/src/TestBA/TestBA.BootstrapperCore.config new file mode 100644 index 00000000..55876a00 --- /dev/null +++ b/src/TestBA/TestBA.BootstrapperCore.config @@ -0,0 +1,18 @@ + + + + + + + +
+ + + + + + + + + + diff --git a/src/TestBA/TestBA.cs b/src/TestBA/TestBA.cs new file mode 100644 index 00000000..1348ce98 --- /dev/null +++ b/src/TestBA/TestBA.cs @@ -0,0 +1,584 @@ +// 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 WixToolset.Test.BA +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading; + using System.Windows.Forms; + using Microsoft.Win32; + using WixToolset.Mba.Core; + + /// + /// A minimal UX used for testing. + /// + public class TestBA : BootstrapperApplication + { + private const string BurnBundleVersionVariable = "WixBundleVersion"; + + private Form dummyWindow; + private IntPtr windowHandle; + private LaunchAction action; + private ManualResetEvent wait; + private int result; + + private string updateBundlePath; + + private bool immediatelyQuit; + private bool quitAfterDetect; + private int redetectRemaining; + private int sleepDuringCache; + private int cancelCacheAtProgress; + private int sleepDuringExecute; + private int cancelExecuteAtProgress; + private string cancelExecuteActionName; + private int cancelOnProgressAtProgress; + private int retryExecuteFilesInUse; + + private IBootstrapperCommand Command { get; } + + private IEngine Engine => this.engine; + + /// + /// Initializes test user experience. + /// + public TestBA(IEngine engine, IBootstrapperCommand bootstrapperCommand) + : base(engine) + { + this.Command = bootstrapperCommand; + this.wait = new ManualResetEvent(false); + } + + /// + /// Get the version of the install. + /// + public string Version { get; private set; } + + /// + /// Indicates if DetectUpdate found a newer version to update. + /// + private bool UpdateAvailable { get; set; } + + /// + /// UI Thread entry point for TestUX. + /// + protected override void OnStartup(StartupEventArgs args) + { + string immediatelyQuit = this.ReadPackageAction(null, "ImmediatelyQuit"); + if (!String.IsNullOrEmpty(immediatelyQuit) && Boolean.TryParse(immediatelyQuit, out this.immediatelyQuit) && this.immediatelyQuit) + { + this.Engine.Quit(0); + return; + } + + base.OnStartup(args); + + this.action = this.Command.Action; + this.TestVariables(); + + this.Version = this.engine.GetVariableVersion(BurnBundleVersionVariable); + this.Log("Version: {0}", this.Version); + + List verifyArguments = this.ReadVerifyArguments(); + + foreach (string arg in this.Command.CommandLineArgs) + { + // If we're not in the update already, process the updatebundle. + if (this.Command.Relation != RelationType.Update && arg.StartsWith("-updatebundle:", StringComparison.OrdinalIgnoreCase)) + { + this.updateBundlePath = arg.Substring(14); + FileInfo info = new FileInfo(this.updateBundlePath); + this.Engine.SetUpdate(this.updateBundlePath, null, info.Length, UpdateHashType.None, null); + this.UpdateAvailable = true; + this.action = LaunchAction.UpdateReplaceEmbedded; + } + else if (this.Command.Relation != RelationType.Update && arg.StartsWith("-checkupdate", StringComparison.OrdinalIgnoreCase)) + { + this.action = LaunchAction.UpdateReplace; + } + + verifyArguments.Remove(arg); + } + this.Log("Action: {0}", this.action); + + // If there are any verification arguments left, error out. + if (0 < verifyArguments.Count) + { + foreach (string expectedArg in verifyArguments) + { + this.Log("Failure. Expected command-line to have argument: {0}", expectedArg); + } + + this.Engine.Quit(-1); + return; + } + + int redetectCount; + string redetect = this.ReadPackageAction(null, "RedetectCount"); + if (String.IsNullOrEmpty(redetect) || !Int32.TryParse(redetect, out redetectCount)) + { + redetectCount = 0; + } + + string quitAfterDetect = this.ReadPackageAction(null, "QuitAfterDetect"); + if (String.IsNullOrEmpty(quitAfterDetect) || !Boolean.TryParse(quitAfterDetect, out this.quitAfterDetect)) + { + this.quitAfterDetect = false; + } + + this.wait.WaitOne(); + + this.redetectRemaining = redetectCount; + for (int i = -1; i < redetectCount; i++) + { + this.Engine.Detect(this.windowHandle); + } + } + + protected override void Run() + { + this.dummyWindow = new Form(); + this.windowHandle = this.dummyWindow.Handle; + + this.Log("Running TestBA application"); + this.wait.Set(); + Application.Run(); + } + + private void ShutdownUiThread() + { + if (this.dummyWindow != null) + { + this.dummyWindow.Invoke(new Action(Application.ExitThread)); + this.dummyWindow.Dispose(); + } + + this.Engine.Quit(this.result & 0xFFFF); // return plain old Win32 error, not HRESULT. + } + + protected override void OnDetectUpdateBegin(DetectUpdateBeginEventArgs args) + { + this.Log("OnDetectUpdateBegin"); + if (LaunchAction.UpdateReplaceEmbedded == this.action || LaunchAction.UpdateReplace == this.action) + { + args.Skip = false; + } + } + + protected override void OnDetectUpdate(DetectUpdateEventArgs e) + { + // The list of updates is sorted in descending version, so the first callback should be the largest update available. + // This update should be either larger than ours (so we are out of date), the same as ours (so we are current) + // or smaller than ours (we have a private build). If we really wanted to, we could leave the e.StopProcessingUpdates alone and + // enumerate all of the updates. + this.Log(String.Format("Potential update v{0} from '{1}'; current version: v{2}", e.Version, e.UpdateLocation, this.Version)); + if (this.Engine.CompareVersions(e.Version, this.Version) > 0) + { + this.Log(String.Format("Selected update v{0}", e.Version)); + this.Engine.SetUpdate(null, e.UpdateLocation, e.Size, UpdateHashType.None, null); + this.UpdateAvailable = true; + } + else + { + this.UpdateAvailable = false; + } + e.StopProcessingUpdates = true; + } + + protected override void OnDetectUpdateComplete(DetectUpdateCompleteEventArgs e) + { + this.Log("OnDetectUpdateComplete"); + + // Failed to process an update, allow the existing bundle to still install. + if (!Hresult.Succeeded(e.Status)) + { + this.Log(String.Format("Failed to locate an update, status of 0x{0:X8}, updates disabled.", e.Status)); + e.IgnoreError = true; // But continue on... + } + } + + protected override void OnDetectComplete(DetectCompleteEventArgs args) + { + this.result = args.Status; + + if (Hresult.Succeeded(this.result) && + (this.UpdateAvailable || LaunchAction.UpdateReplaceEmbedded != this.action && LaunchAction.UpdateReplace != this.action)) + { + if (this.redetectRemaining > 0) + { + this.Log("Completed detection phase: {0} re-runs remaining", this.redetectRemaining--); + } + else if (this.quitAfterDetect) + { + this.ShutdownUiThread(); + } + else + { + this.Engine.Plan(this.action); + } + } + else + { + this.ShutdownUiThread(); + } + } + + protected override void OnPlanPackageBegin(PlanPackageBeginEventArgs args) + { + RequestState state; + string action = this.ReadPackageAction(args.PackageId, "Requested"); + if (TryParseEnum(action, out state)) + { + args.State = state; + } + } + + protected override void OnPlanTargetMsiPackage(PlanTargetMsiPackageEventArgs args) + { + RequestState state; + string action = this.ReadPackageAction(args.PackageId, "Requested"); + if (TryParseEnum(action, out state)) + { + args.State = state; + } + } + + protected override void OnPlanMsiFeature(PlanMsiFeatureEventArgs args) + { + FeatureState state; + string action = this.ReadFeatureAction(args.PackageId, args.FeatureId, "Requested"); + if (TryParseEnum(action, out state)) + { + args.State = state; + } + } + + protected override void OnPlanComplete(PlanCompleteEventArgs args) + { + this.result = args.Status; + if (Hresult.Succeeded(this.result)) + { + this.Engine.Apply(this.windowHandle); + } + else + { + this.ShutdownUiThread(); + } + } + + protected override void OnCachePackageBegin(CachePackageBeginEventArgs args) + { + this.Log("OnCachePackageBegin() - package: {0}, payloads to cache: {1}", args.PackageId, args.CachePayloads); + + string slowProgress = this.ReadPackageAction(args.PackageId, "SlowCache"); + if (String.IsNullOrEmpty(slowProgress) || !Int32.TryParse(slowProgress, out this.sleepDuringCache)) + { + this.sleepDuringCache = 0; + } + else + { + this.Log(" SlowCache: {0}", this.sleepDuringCache); + } + + string cancelCache = this.ReadPackageAction(args.PackageId, "CancelCacheAtProgress"); + if (String.IsNullOrEmpty(cancelCache) || !Int32.TryParse(cancelCache, out this.cancelCacheAtProgress)) + { + this.cancelCacheAtProgress = -1; + } + else + { + this.Log(" CancelCacheAtProgress: {0}", this.cancelCacheAtProgress); + } + } + + protected override void OnCacheAcquireProgress(CacheAcquireProgressEventArgs args) + { + this.Log("OnCacheAcquireProgress() - container/package: {0}, payload: {1}, progress: {2}, total: {3}, overall progress: {4}%", args.PackageOrContainerId, args.PayloadId, args.Progress, args.Total, args.OverallPercentage); + + if (this.cancelCacheAtProgress >= 0 && this.cancelCacheAtProgress <= args.Progress) + { + args.Cancel = true; + this.Log("OnCacheAcquireProgress(cancel)"); + } + else if (this.sleepDuringCache > 0) + { + this.Log("OnCacheAcquireProgress(sleep {0})", this.sleepDuringCache); + Thread.Sleep(this.sleepDuringCache); + } + } + + protected override void OnExecutePackageBegin(ExecutePackageBeginEventArgs args) + { + this.Log("OnExecutePackageBegin() - package: {0}, rollback: {1}", args.PackageId, !args.ShouldExecute); + + string slowProgress = this.ReadPackageAction(args.PackageId, "SlowExecute"); + if (String.IsNullOrEmpty(slowProgress) || !Int32.TryParse(slowProgress, out this.sleepDuringExecute)) + { + this.sleepDuringExecute = 0; + } + else + { + this.Log(" SlowExecute: {0}", this.sleepDuringExecute); + } + + string cancelExecute = this.ReadPackageAction(args.PackageId, "CancelExecuteAtProgress"); + if (String.IsNullOrEmpty(cancelExecute) || !Int32.TryParse(cancelExecute, out this.cancelExecuteAtProgress)) + { + this.cancelExecuteAtProgress = -1; + } + else + { + this.Log(" CancelExecuteAtProgress: {0}", this.cancelExecuteAtProgress); + } + + this.cancelExecuteActionName = this.ReadPackageAction(args.PackageId, "CancelExecuteAtActionStart"); + if (!String.IsNullOrEmpty(this.cancelExecuteActionName)) + { + this.Log(" CancelExecuteAtActionState: {0}", this.cancelExecuteActionName); + } + + string cancelOnProgressAtProgress = this.ReadPackageAction(args.PackageId, "CancelOnProgressAtProgress"); + if (String.IsNullOrEmpty(cancelOnProgressAtProgress) || !Int32.TryParse(cancelOnProgressAtProgress, out this.cancelOnProgressAtProgress)) + { + this.cancelOnProgressAtProgress = -1; + } + else + { + this.Log(" CancelOnProgressAtProgress: {0}", this.cancelOnProgressAtProgress); + } + + string retryBeforeCancel = this.ReadPackageAction(args.PackageId, "RetryExecuteFilesInUse"); + if (String.IsNullOrEmpty(retryBeforeCancel) || !Int32.TryParse(retryBeforeCancel, out this.retryExecuteFilesInUse)) + { + this.retryExecuteFilesInUse = 0; + } + else + { + this.Log(" RetryExecuteFilesInUse: {0}", this.retryExecuteFilesInUse); + } + } + + protected override void OnExecuteFilesInUse(ExecuteFilesInUseEventArgs args) + { + this.Log("OnExecuteFilesInUse() - package: {0}, retries remaining: {1}, data: {2}", args.PackageId, this.retryExecuteFilesInUse, String.Join(", ", args.Files.ToArray())); + + if (this.retryExecuteFilesInUse > 0) + { + --this.retryExecuteFilesInUse; + args.Result = Result.Retry; + } + else + { + args.Result = Result.Abort; + } + } + + protected override void OnExecuteMsiMessage(ExecuteMsiMessageEventArgs args) + { + this.Log("OnExecuteMsiMessage() - MessageType: {0}, Message: {1}, Data: '{2}'", args.MessageType, args.Message, String.Join("','", args.Data.ToArray())); + + if (!String.IsNullOrEmpty(this.cancelExecuteActionName) && args.MessageType == InstallMessage.ActionStart && + args.Data.Count > 0 && args.Data[0] == this.cancelExecuteActionName) + { + this.Log("OnExecuteMsiMessage(cancelNextProgress)"); + this.cancelExecuteAtProgress = 0; + } + } + + protected override void OnExecuteProgress(ExecuteProgressEventArgs args) + { + this.Log("OnExecuteProgress() - package: {0}, progress: {1}%, overall progress: {2}%", args.PackageId, args.ProgressPercentage, args.OverallPercentage); + + if (this.cancelExecuteAtProgress >= 0 && this.cancelExecuteAtProgress <= args.ProgressPercentage) + { + args.Cancel = true; + this.Log("OnExecuteProgress(cancel)"); + } + else if (this.sleepDuringExecute > 0) + { + this.Log("OnExecuteProgress(sleep {0})", this.sleepDuringExecute); + Thread.Sleep(this.sleepDuringExecute); + } + } + + protected override void OnExecutePatchTarget(ExecutePatchTargetEventArgs args) + { + this.Log("OnExecutePatchTarget - Patch Package: {0}, Target Product Code: {1}", args.PackageId, args.TargetProductCode); + } + + protected override void OnProgress(ProgressEventArgs args) + { + this.Log("OnProgress() - progress: {0}%, overall progress: {1}%", args.ProgressPercentage, args.OverallPercentage); + if (this.Command.Display == Display.Embedded) + { + this.Engine.SendEmbeddedProgress(args.ProgressPercentage, args.OverallPercentage); + } + + if (this.cancelOnProgressAtProgress >= 0 && this.cancelOnProgressAtProgress <= args.OverallPercentage) + { + args.Cancel = true; + this.Log("OnProgress(cancel)"); + } + } + + protected override void OnResolveSource(ResolveSourceEventArgs args) + { + if (!String.IsNullOrEmpty(args.DownloadSource)) + { + args.Action = BOOTSTRAPPER_RESOLVESOURCE_ACTION.Download; + } + } + + protected override void OnApplyBegin(ApplyBeginEventArgs args) + { + this.cancelOnProgressAtProgress = -1; + this.cancelExecuteAtProgress = -1; + this.cancelCacheAtProgress = -1; + } + + protected override void OnApplyComplete(ApplyCompleteEventArgs args) + { + // Output what the privileges are now. + this.Log("After elevation: WixBundleElevated = {0}", this.Engine.GetVariableNumeric("WixBundleElevated")); + + this.result = args.Status; + this.ShutdownUiThread(); + } + + protected override void OnSystemShutdown(SystemShutdownEventArgs args) + { + // Always prevent shutdown. + this.Log("Disallowed system request to shut down the bootstrapper application."); + args.Cancel = true; + + this.ShutdownUiThread(); + } + + private void TestVariables() + { + // First make sure we can check and get standard variables of each type. + { + string value = null; + if (this.Engine.ContainsVariable("WindowsFolder")) + { + value = this.Engine.GetVariableString("WindowsFolder"); + this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a string variable: WindowsFolder"); + } + else + { + throw new Exception("Engine did not define a standard variable: WindowsFolder"); + } + } + + { + long value = 0; + if (this.Engine.ContainsVariable("NTProductType")) + { + value = this.Engine.GetVariableNumeric("NTProductType"); + this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a numeric variable: NTProductType"); + } + else + { + throw new Exception("Engine did not define a standard variable: NTProductType"); + } + } + + { + string value = null; + if (this.Engine.ContainsVariable("VersionMsi")) + { + value = this.Engine.GetVariableVersion("VersionMsi"); + this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a version variable: VersionMsi"); + } + else + { + throw new Exception("Engine did not define a standard variable: VersionMsi"); + } + } + + // Now validate that Contians returns false for non-existant variables of each type. + if (this.Engine.ContainsVariable("TestStringVariableShouldNotExist")) + { + throw new Exception("Engine defined a variable that should not exist: TestStringVariableShouldNotExist"); + } + else + { + this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent string variable: TestStringVariableShouldNotExist"); + } + + if (this.Engine.ContainsVariable("TestNumericVariableShouldNotExist")) + { + throw new Exception("Engine defined a variable that should not exist: TestNumericVariableShouldNotExist"); + } + else + { + this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent numeric variable: TestNumericVariableShouldNotExist"); + } + + if (this.Engine.ContainsVariable("TestVersionVariableShouldNotExist")) + { + throw new Exception("Engine defined a variable that should not exist: TestVersionVariableShouldNotExist"); + } + else + { + this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent version variable: TestVersionVariableShouldNotExist"); + } + + // Output what the initially run privileges were. + this.Engine.Log(LogLevel.Verbose, String.Format("TEST: WixBundleElevated = {0}", this.Engine.GetVariableNumeric("WixBundleElevated"))); + } + + private void Log(string format, params object[] args) + { + string relation = this.Command.Relation != RelationType.None ? String.Concat(" (", this.Command.Relation.ToString().ToLowerInvariant(), ")") : String.Empty; + string message = String.Format(format, args); + + this.Engine.Log(LogLevel.Standard, String.Concat("TESTBA", relation, ": ", message)); + } + + private List ReadVerifyArguments() + { + string testName = this.Engine.GetVariableString("TestGroupName"); + using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}", testName))) + { + string verifyArguments = testKey == null ? null : testKey.GetValue("VerifyArguments") as string; + return verifyArguments == null ? new List() : new List(verifyArguments.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); + } + } + + private string ReadPackageAction(string packageId, string state) + { + string testName = this.Engine.GetVariableString("TestGroupName"); + using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}\{1}", testName, String.IsNullOrEmpty(packageId) ? String.Empty : packageId))) + { + return testKey == null ? null : testKey.GetValue(state) as string; + } + } + + private string ReadFeatureAction(string packageId, string featureId, string state) + { + string testName = this.Engine.GetVariableString("TestGroupName"); + using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}\{1}", testName, packageId))) + { + string registryName = String.Concat(featureId, state); + return testKey == null ? null : testKey.GetValue(registryName) as string; + } + } + + private static bool TryParseEnum(string value, out T t) + { + try + { + t = (T)Enum.Parse(typeof(T), value, true); + return true; + } + catch (ArgumentException) { } + catch (OverflowException) { } + + t = default(T); + return false; + } + } +} diff --git a/src/TestBA/TestBA.csproj b/src/TestBA/TestBA.csproj new file mode 100644 index 00000000..b6aea84b --- /dev/null +++ b/src/TestBA/TestBA.csproj @@ -0,0 +1,24 @@ + + + + + + net35;net5.0-windows + TestBA + WixToolset.Test.BA + embedded + win-x86 + true + true + Major + + + + + + + + + + + \ No newline at end of file diff --git a/src/TestBA/TestBAFactory.cs b/src/TestBA/TestBAFactory.cs new file mode 100644 index 00000000..ba1de367 --- /dev/null +++ b/src/TestBA/TestBAFactory.cs @@ -0,0 +1,22 @@ +// 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. + +[assembly: WixToolset.Mba.Core.BootstrapperApplicationFactory(typeof(WixToolset.Test.BA.TestBAFactory))] +namespace WixToolset.Test.BA +{ + using WixToolset.Mba.Core; + + public class TestBAFactory : BaseBootstrapperApplicationFactory + { + private static int loadCount = 0; + + protected override IBootstrapperApplication Create(IEngine engine, IBootstrapperCommand bootstrapperCommand) + { + if (loadCount > 0) + { + engine.Log(LogLevel.Standard, $"Reloaded {loadCount} time(s)"); + } + ++loadCount; + return new TestBA(engine, bootstrapperCommand); + } + } +} diff --git a/src/TestData/TestBA/TestBAWixlib/testbawixlib.wixproj b/src/TestData/TestBA/TestBAWixlib/testbawixlib.wixproj index 71ff3cb1..85ad69b3 100644 --- a/src/TestData/TestBA/TestBAWixlib/testbawixlib.wixproj +++ b/src/TestData/TestBA/TestBAWixlib/testbawixlib.wixproj @@ -10,7 +10,7 @@ - + diff --git a/src/Utilities/TestBA/Hresult.cs b/src/Utilities/TestBA/Hresult.cs deleted file mode 100644 index bc1aa8c0..00000000 --- a/src/Utilities/TestBA/Hresult.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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 WixToolset.Test.BA -{ - using System; - - /// - /// Utility class to work with HRESULTs - /// - internal class Hresult - { - /// - /// Determines if an HRESULT was a success code or not. - /// - /// HRESULT to verify. - /// True if the status is a success code. - public static bool Succeeded(int status) - { - return status >= 0; - } - } -} diff --git a/src/Utilities/TestBA/TestBA.BootstrapperCore.config b/src/Utilities/TestBA/TestBA.BootstrapperCore.config deleted file mode 100644 index 55876a00..00000000 --- a/src/Utilities/TestBA/TestBA.BootstrapperCore.config +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - -
- - - - - - - - - - diff --git a/src/Utilities/TestBA/TestBA.cs b/src/Utilities/TestBA/TestBA.cs deleted file mode 100644 index 1348ce98..00000000 --- a/src/Utilities/TestBA/TestBA.cs +++ /dev/null @@ -1,584 +0,0 @@ -// 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 WixToolset.Test.BA -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Threading; - using System.Windows.Forms; - using Microsoft.Win32; - using WixToolset.Mba.Core; - - /// - /// A minimal UX used for testing. - /// - public class TestBA : BootstrapperApplication - { - private const string BurnBundleVersionVariable = "WixBundleVersion"; - - private Form dummyWindow; - private IntPtr windowHandle; - private LaunchAction action; - private ManualResetEvent wait; - private int result; - - private string updateBundlePath; - - private bool immediatelyQuit; - private bool quitAfterDetect; - private int redetectRemaining; - private int sleepDuringCache; - private int cancelCacheAtProgress; - private int sleepDuringExecute; - private int cancelExecuteAtProgress; - private string cancelExecuteActionName; - private int cancelOnProgressAtProgress; - private int retryExecuteFilesInUse; - - private IBootstrapperCommand Command { get; } - - private IEngine Engine => this.engine; - - /// - /// Initializes test user experience. - /// - public TestBA(IEngine engine, IBootstrapperCommand bootstrapperCommand) - : base(engine) - { - this.Command = bootstrapperCommand; - this.wait = new ManualResetEvent(false); - } - - /// - /// Get the version of the install. - /// - public string Version { get; private set; } - - /// - /// Indicates if DetectUpdate found a newer version to update. - /// - private bool UpdateAvailable { get; set; } - - /// - /// UI Thread entry point for TestUX. - /// - protected override void OnStartup(StartupEventArgs args) - { - string immediatelyQuit = this.ReadPackageAction(null, "ImmediatelyQuit"); - if (!String.IsNullOrEmpty(immediatelyQuit) && Boolean.TryParse(immediatelyQuit, out this.immediatelyQuit) && this.immediatelyQuit) - { - this.Engine.Quit(0); - return; - } - - base.OnStartup(args); - - this.action = this.Command.Action; - this.TestVariables(); - - this.Version = this.engine.GetVariableVersion(BurnBundleVersionVariable); - this.Log("Version: {0}", this.Version); - - List verifyArguments = this.ReadVerifyArguments(); - - foreach (string arg in this.Command.CommandLineArgs) - { - // If we're not in the update already, process the updatebundle. - if (this.Command.Relation != RelationType.Update && arg.StartsWith("-updatebundle:", StringComparison.OrdinalIgnoreCase)) - { - this.updateBundlePath = arg.Substring(14); - FileInfo info = new FileInfo(this.updateBundlePath); - this.Engine.SetUpdate(this.updateBundlePath, null, info.Length, UpdateHashType.None, null); - this.UpdateAvailable = true; - this.action = LaunchAction.UpdateReplaceEmbedded; - } - else if (this.Command.Relation != RelationType.Update && arg.StartsWith("-checkupdate", StringComparison.OrdinalIgnoreCase)) - { - this.action = LaunchAction.UpdateReplace; - } - - verifyArguments.Remove(arg); - } - this.Log("Action: {0}", this.action); - - // If there are any verification arguments left, error out. - if (0 < verifyArguments.Count) - { - foreach (string expectedArg in verifyArguments) - { - this.Log("Failure. Expected command-line to have argument: {0}", expectedArg); - } - - this.Engine.Quit(-1); - return; - } - - int redetectCount; - string redetect = this.ReadPackageAction(null, "RedetectCount"); - if (String.IsNullOrEmpty(redetect) || !Int32.TryParse(redetect, out redetectCount)) - { - redetectCount = 0; - } - - string quitAfterDetect = this.ReadPackageAction(null, "QuitAfterDetect"); - if (String.IsNullOrEmpty(quitAfterDetect) || !Boolean.TryParse(quitAfterDetect, out this.quitAfterDetect)) - { - this.quitAfterDetect = false; - } - - this.wait.WaitOne(); - - this.redetectRemaining = redetectCount; - for (int i = -1; i < redetectCount; i++) - { - this.Engine.Detect(this.windowHandle); - } - } - - protected override void Run() - { - this.dummyWindow = new Form(); - this.windowHandle = this.dummyWindow.Handle; - - this.Log("Running TestBA application"); - this.wait.Set(); - Application.Run(); - } - - private void ShutdownUiThread() - { - if (this.dummyWindow != null) - { - this.dummyWindow.Invoke(new Action(Application.ExitThread)); - this.dummyWindow.Dispose(); - } - - this.Engine.Quit(this.result & 0xFFFF); // return plain old Win32 error, not HRESULT. - } - - protected override void OnDetectUpdateBegin(DetectUpdateBeginEventArgs args) - { - this.Log("OnDetectUpdateBegin"); - if (LaunchAction.UpdateReplaceEmbedded == this.action || LaunchAction.UpdateReplace == this.action) - { - args.Skip = false; - } - } - - protected override void OnDetectUpdate(DetectUpdateEventArgs e) - { - // The list of updates is sorted in descending version, so the first callback should be the largest update available. - // This update should be either larger than ours (so we are out of date), the same as ours (so we are current) - // or smaller than ours (we have a private build). If we really wanted to, we could leave the e.StopProcessingUpdates alone and - // enumerate all of the updates. - this.Log(String.Format("Potential update v{0} from '{1}'; current version: v{2}", e.Version, e.UpdateLocation, this.Version)); - if (this.Engine.CompareVersions(e.Version, this.Version) > 0) - { - this.Log(String.Format("Selected update v{0}", e.Version)); - this.Engine.SetUpdate(null, e.UpdateLocation, e.Size, UpdateHashType.None, null); - this.UpdateAvailable = true; - } - else - { - this.UpdateAvailable = false; - } - e.StopProcessingUpdates = true; - } - - protected override void OnDetectUpdateComplete(DetectUpdateCompleteEventArgs e) - { - this.Log("OnDetectUpdateComplete"); - - // Failed to process an update, allow the existing bundle to still install. - if (!Hresult.Succeeded(e.Status)) - { - this.Log(String.Format("Failed to locate an update, status of 0x{0:X8}, updates disabled.", e.Status)); - e.IgnoreError = true; // But continue on... - } - } - - protected override void OnDetectComplete(DetectCompleteEventArgs args) - { - this.result = args.Status; - - if (Hresult.Succeeded(this.result) && - (this.UpdateAvailable || LaunchAction.UpdateReplaceEmbedded != this.action && LaunchAction.UpdateReplace != this.action)) - { - if (this.redetectRemaining > 0) - { - this.Log("Completed detection phase: {0} re-runs remaining", this.redetectRemaining--); - } - else if (this.quitAfterDetect) - { - this.ShutdownUiThread(); - } - else - { - this.Engine.Plan(this.action); - } - } - else - { - this.ShutdownUiThread(); - } - } - - protected override void OnPlanPackageBegin(PlanPackageBeginEventArgs args) - { - RequestState state; - string action = this.ReadPackageAction(args.PackageId, "Requested"); - if (TryParseEnum(action, out state)) - { - args.State = state; - } - } - - protected override void OnPlanTargetMsiPackage(PlanTargetMsiPackageEventArgs args) - { - RequestState state; - string action = this.ReadPackageAction(args.PackageId, "Requested"); - if (TryParseEnum(action, out state)) - { - args.State = state; - } - } - - protected override void OnPlanMsiFeature(PlanMsiFeatureEventArgs args) - { - FeatureState state; - string action = this.ReadFeatureAction(args.PackageId, args.FeatureId, "Requested"); - if (TryParseEnum(action, out state)) - { - args.State = state; - } - } - - protected override void OnPlanComplete(PlanCompleteEventArgs args) - { - this.result = args.Status; - if (Hresult.Succeeded(this.result)) - { - this.Engine.Apply(this.windowHandle); - } - else - { - this.ShutdownUiThread(); - } - } - - protected override void OnCachePackageBegin(CachePackageBeginEventArgs args) - { - this.Log("OnCachePackageBegin() - package: {0}, payloads to cache: {1}", args.PackageId, args.CachePayloads); - - string slowProgress = this.ReadPackageAction(args.PackageId, "SlowCache"); - if (String.IsNullOrEmpty(slowProgress) || !Int32.TryParse(slowProgress, out this.sleepDuringCache)) - { - this.sleepDuringCache = 0; - } - else - { - this.Log(" SlowCache: {0}", this.sleepDuringCache); - } - - string cancelCache = this.ReadPackageAction(args.PackageId, "CancelCacheAtProgress"); - if (String.IsNullOrEmpty(cancelCache) || !Int32.TryParse(cancelCache, out this.cancelCacheAtProgress)) - { - this.cancelCacheAtProgress = -1; - } - else - { - this.Log(" CancelCacheAtProgress: {0}", this.cancelCacheAtProgress); - } - } - - protected override void OnCacheAcquireProgress(CacheAcquireProgressEventArgs args) - { - this.Log("OnCacheAcquireProgress() - container/package: {0}, payload: {1}, progress: {2}, total: {3}, overall progress: {4}%", args.PackageOrContainerId, args.PayloadId, args.Progress, args.Total, args.OverallPercentage); - - if (this.cancelCacheAtProgress >= 0 && this.cancelCacheAtProgress <= args.Progress) - { - args.Cancel = true; - this.Log("OnCacheAcquireProgress(cancel)"); - } - else if (this.sleepDuringCache > 0) - { - this.Log("OnCacheAcquireProgress(sleep {0})", this.sleepDuringCache); - Thread.Sleep(this.sleepDuringCache); - } - } - - protected override void OnExecutePackageBegin(ExecutePackageBeginEventArgs args) - { - this.Log("OnExecutePackageBegin() - package: {0}, rollback: {1}", args.PackageId, !args.ShouldExecute); - - string slowProgress = this.ReadPackageAction(args.PackageId, "SlowExecute"); - if (String.IsNullOrEmpty(slowProgress) || !Int32.TryParse(slowProgress, out this.sleepDuringExecute)) - { - this.sleepDuringExecute = 0; - } - else - { - this.Log(" SlowExecute: {0}", this.sleepDuringExecute); - } - - string cancelExecute = this.ReadPackageAction(args.PackageId, "CancelExecuteAtProgress"); - if (String.IsNullOrEmpty(cancelExecute) || !Int32.TryParse(cancelExecute, out this.cancelExecuteAtProgress)) - { - this.cancelExecuteAtProgress = -1; - } - else - { - this.Log(" CancelExecuteAtProgress: {0}", this.cancelExecuteAtProgress); - } - - this.cancelExecuteActionName = this.ReadPackageAction(args.PackageId, "CancelExecuteAtActionStart"); - if (!String.IsNullOrEmpty(this.cancelExecuteActionName)) - { - this.Log(" CancelExecuteAtActionState: {0}", this.cancelExecuteActionName); - } - - string cancelOnProgressAtProgress = this.ReadPackageAction(args.PackageId, "CancelOnProgressAtProgress"); - if (String.IsNullOrEmpty(cancelOnProgressAtProgress) || !Int32.TryParse(cancelOnProgressAtProgress, out this.cancelOnProgressAtProgress)) - { - this.cancelOnProgressAtProgress = -1; - } - else - { - this.Log(" CancelOnProgressAtProgress: {0}", this.cancelOnProgressAtProgress); - } - - string retryBeforeCancel = this.ReadPackageAction(args.PackageId, "RetryExecuteFilesInUse"); - if (String.IsNullOrEmpty(retryBeforeCancel) || !Int32.TryParse(retryBeforeCancel, out this.retryExecuteFilesInUse)) - { - this.retryExecuteFilesInUse = 0; - } - else - { - this.Log(" RetryExecuteFilesInUse: {0}", this.retryExecuteFilesInUse); - } - } - - protected override void OnExecuteFilesInUse(ExecuteFilesInUseEventArgs args) - { - this.Log("OnExecuteFilesInUse() - package: {0}, retries remaining: {1}, data: {2}", args.PackageId, this.retryExecuteFilesInUse, String.Join(", ", args.Files.ToArray())); - - if (this.retryExecuteFilesInUse > 0) - { - --this.retryExecuteFilesInUse; - args.Result = Result.Retry; - } - else - { - args.Result = Result.Abort; - } - } - - protected override void OnExecuteMsiMessage(ExecuteMsiMessageEventArgs args) - { - this.Log("OnExecuteMsiMessage() - MessageType: {0}, Message: {1}, Data: '{2}'", args.MessageType, args.Message, String.Join("','", args.Data.ToArray())); - - if (!String.IsNullOrEmpty(this.cancelExecuteActionName) && args.MessageType == InstallMessage.ActionStart && - args.Data.Count > 0 && args.Data[0] == this.cancelExecuteActionName) - { - this.Log("OnExecuteMsiMessage(cancelNextProgress)"); - this.cancelExecuteAtProgress = 0; - } - } - - protected override void OnExecuteProgress(ExecuteProgressEventArgs args) - { - this.Log("OnExecuteProgress() - package: {0}, progress: {1}%, overall progress: {2}%", args.PackageId, args.ProgressPercentage, args.OverallPercentage); - - if (this.cancelExecuteAtProgress >= 0 && this.cancelExecuteAtProgress <= args.ProgressPercentage) - { - args.Cancel = true; - this.Log("OnExecuteProgress(cancel)"); - } - else if (this.sleepDuringExecute > 0) - { - this.Log("OnExecuteProgress(sleep {0})", this.sleepDuringExecute); - Thread.Sleep(this.sleepDuringExecute); - } - } - - protected override void OnExecutePatchTarget(ExecutePatchTargetEventArgs args) - { - this.Log("OnExecutePatchTarget - Patch Package: {0}, Target Product Code: {1}", args.PackageId, args.TargetProductCode); - } - - protected override void OnProgress(ProgressEventArgs args) - { - this.Log("OnProgress() - progress: {0}%, overall progress: {1}%", args.ProgressPercentage, args.OverallPercentage); - if (this.Command.Display == Display.Embedded) - { - this.Engine.SendEmbeddedProgress(args.ProgressPercentage, args.OverallPercentage); - } - - if (this.cancelOnProgressAtProgress >= 0 && this.cancelOnProgressAtProgress <= args.OverallPercentage) - { - args.Cancel = true; - this.Log("OnProgress(cancel)"); - } - } - - protected override void OnResolveSource(ResolveSourceEventArgs args) - { - if (!String.IsNullOrEmpty(args.DownloadSource)) - { - args.Action = BOOTSTRAPPER_RESOLVESOURCE_ACTION.Download; - } - } - - protected override void OnApplyBegin(ApplyBeginEventArgs args) - { - this.cancelOnProgressAtProgress = -1; - this.cancelExecuteAtProgress = -1; - this.cancelCacheAtProgress = -1; - } - - protected override void OnApplyComplete(ApplyCompleteEventArgs args) - { - // Output what the privileges are now. - this.Log("After elevation: WixBundleElevated = {0}", this.Engine.GetVariableNumeric("WixBundleElevated")); - - this.result = args.Status; - this.ShutdownUiThread(); - } - - protected override void OnSystemShutdown(SystemShutdownEventArgs args) - { - // Always prevent shutdown. - this.Log("Disallowed system request to shut down the bootstrapper application."); - args.Cancel = true; - - this.ShutdownUiThread(); - } - - private void TestVariables() - { - // First make sure we can check and get standard variables of each type. - { - string value = null; - if (this.Engine.ContainsVariable("WindowsFolder")) - { - value = this.Engine.GetVariableString("WindowsFolder"); - this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a string variable: WindowsFolder"); - } - else - { - throw new Exception("Engine did not define a standard variable: WindowsFolder"); - } - } - - { - long value = 0; - if (this.Engine.ContainsVariable("NTProductType")) - { - value = this.Engine.GetVariableNumeric("NTProductType"); - this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a numeric variable: NTProductType"); - } - else - { - throw new Exception("Engine did not define a standard variable: NTProductType"); - } - } - - { - string value = null; - if (this.Engine.ContainsVariable("VersionMsi")) - { - value = this.Engine.GetVariableVersion("VersionMsi"); - this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a version variable: VersionMsi"); - } - else - { - throw new Exception("Engine did not define a standard variable: VersionMsi"); - } - } - - // Now validate that Contians returns false for non-existant variables of each type. - if (this.Engine.ContainsVariable("TestStringVariableShouldNotExist")) - { - throw new Exception("Engine defined a variable that should not exist: TestStringVariableShouldNotExist"); - } - else - { - this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent string variable: TestStringVariableShouldNotExist"); - } - - if (this.Engine.ContainsVariable("TestNumericVariableShouldNotExist")) - { - throw new Exception("Engine defined a variable that should not exist: TestNumericVariableShouldNotExist"); - } - else - { - this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent numeric variable: TestNumericVariableShouldNotExist"); - } - - if (this.Engine.ContainsVariable("TestVersionVariableShouldNotExist")) - { - throw new Exception("Engine defined a variable that should not exist: TestVersionVariableShouldNotExist"); - } - else - { - this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent version variable: TestVersionVariableShouldNotExist"); - } - - // Output what the initially run privileges were. - this.Engine.Log(LogLevel.Verbose, String.Format("TEST: WixBundleElevated = {0}", this.Engine.GetVariableNumeric("WixBundleElevated"))); - } - - private void Log(string format, params object[] args) - { - string relation = this.Command.Relation != RelationType.None ? String.Concat(" (", this.Command.Relation.ToString().ToLowerInvariant(), ")") : String.Empty; - string message = String.Format(format, args); - - this.Engine.Log(LogLevel.Standard, String.Concat("TESTBA", relation, ": ", message)); - } - - private List ReadVerifyArguments() - { - string testName = this.Engine.GetVariableString("TestGroupName"); - using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}", testName))) - { - string verifyArguments = testKey == null ? null : testKey.GetValue("VerifyArguments") as string; - return verifyArguments == null ? new List() : new List(verifyArguments.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)); - } - } - - private string ReadPackageAction(string packageId, string state) - { - string testName = this.Engine.GetVariableString("TestGroupName"); - using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}\{1}", testName, String.IsNullOrEmpty(packageId) ? String.Empty : packageId))) - { - return testKey == null ? null : testKey.GetValue(state) as string; - } - } - - private string ReadFeatureAction(string packageId, string featureId, string state) - { - string testName = this.Engine.GetVariableString("TestGroupName"); - using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}\{1}", testName, packageId))) - { - string registryName = String.Concat(featureId, state); - return testKey == null ? null : testKey.GetValue(registryName) as string; - } - } - - private static bool TryParseEnum(string value, out T t) - { - try - { - t = (T)Enum.Parse(typeof(T), value, true); - return true; - } - catch (ArgumentException) { } - catch (OverflowException) { } - - t = default(T); - return false; - } - } -} diff --git a/src/Utilities/TestBA/TestBA.csproj b/src/Utilities/TestBA/TestBA.csproj deleted file mode 100644 index b6aea84b..00000000 --- a/src/Utilities/TestBA/TestBA.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - net35;net5.0-windows - TestBA - WixToolset.Test.BA - embedded - win-x86 - true - true - Major - - - - - - - - - - - \ No newline at end of file diff --git a/src/Utilities/TestBA/TestBAFactory.cs b/src/Utilities/TestBA/TestBAFactory.cs deleted file mode 100644 index ba1de367..00000000 --- a/src/Utilities/TestBA/TestBAFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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. - -[assembly: WixToolset.Mba.Core.BootstrapperApplicationFactory(typeof(WixToolset.Test.BA.TestBAFactory))] -namespace WixToolset.Test.BA -{ - using WixToolset.Mba.Core; - - public class TestBAFactory : BaseBootstrapperApplicationFactory - { - private static int loadCount = 0; - - protected override IBootstrapperApplication Create(IEngine engine, IBootstrapperCommand bootstrapperCommand) - { - if (loadCount > 0) - { - engine.Log(LogLevel.Standard, $"Reloaded {loadCount} time(s)"); - } - ++loadCount; - return new TestBA(engine, bootstrapperCommand); - } - } -} diff --git a/src/WixTestTools/BundleInstaller.cs b/src/WixTestTools/BundleInstaller.cs new file mode 100644 index 00000000..044486fe --- /dev/null +++ b/src/WixTestTools/BundleInstaller.cs @@ -0,0 +1,141 @@ +// 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 WixTestTools +{ + using System; + using System.IO; + using System.Text; + + public partial class BundleInstaller : IDisposable + { + public BundleInstaller(WixTestContext testContext, string name) + { + this.Bundle = Path.Combine(testContext.TestDataFolder, $"{name}.exe"); + this.BundlePdb = Path.Combine(testContext.TestDataFolder, $"{name}.wixpdb"); + this.TestGroupName = testContext.TestGroupName; + this.TestName = testContext.TestName; + } + + public string Bundle { get; } + + public string TestGroupName { get; } + + public string TestName { get; } + + /// + /// Installs the bundle with optional arguments. + /// + /// Expected exit code, defaults to success. + /// Optional arguments to pass to the tool. + /// Path to the generated log file. + public string Install(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) + { + return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Install, arguments); + } + + /// + /// Modify the bundle with optional arguments. + /// + /// Expected exit code, defaults to success. + /// Optional arguments to pass to the tool. + /// Path to the generated log file. + public string Modify(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) + { + return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Modify, arguments); + } + + /// + /// Repairs the bundle with optional arguments. + /// + /// Expected exit code, defaults to success. + /// Optional arguments to pass to the tool. + /// Path to the generated log file. + public string Repair(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) + { + return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Repair, arguments); + } + + /// + /// Uninstalls the bundle with optional arguments. + /// + /// Expected exit code, defaults to success. + /// Optional arguments to pass to the tool. + /// Path to the generated log file. + public string Uninstall(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) + { + return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Uninstall, arguments); + } + + /// + /// Uninstalls the bundle at the given path with optional arguments. + /// + /// This should be the bundle in the package cache. + /// Expected exit code, defaults to success. + /// Optional arguments to pass to the tool. + /// Path to the generated log file. + public string Uninstall(string bundlePath, int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) + { + return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Uninstall, arguments, bundlePath: bundlePath); + } + + /// + /// Executes the bundle with optional arguments. + /// + /// Expected exit code. + /// Install mode. + /// Optional arguments to pass to the tool. + /// Path to the generated log file. + private string RunBundleWithArguments(int expectedExitCode, MSIExec.MSIExecMode mode, string[] arguments, bool assertOnError = true, string bundlePath = null) + { + TestTool bundle = new TestTool(bundlePath ?? this.Bundle); + var sb = new StringBuilder(); + + // Be sure to run silent. + 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)); + sb.AppendFormat(" -log \"{0}\"", logFile); + + // Set operation. + switch (mode) + { + case MSIExec.MSIExecMode.Modify: + sb.Append(" -modify"); + break; + + case MSIExec.MSIExecMode.Repair: + sb.Append(" -repair"); + break; + + case MSIExec.MSIExecMode.Cleanup: + case MSIExec.MSIExecMode.Uninstall: + sb.Append(" -uninstall"); + break; + } + + // Add additional arguments. + if (null != arguments) + { + sb.Append(" "); + sb.Append(String.Join(" ", arguments)); + } + + // Set the arguments. + bundle.Arguments = sb.ToString(); + + // Run the tool and assert the expected code. + bundle.ExpectedExitCode = expectedExitCode; + bundle.Run(assertOnError); + + // Return the log file name. + return logFile; + } + + public void Dispose() + { + string[] args = { "-burn.ignoredependencies=ALL" }; + this.RunBundleWithArguments((int)MSIExec.MSIExecReturnCode.SUCCESS, MSIExec.MSIExecMode.Cleanup, args, assertOnError: false); + } + } +} diff --git a/src/WixTestTools/BundleRegistration.cs b/src/WixTestTools/BundleRegistration.cs new file mode 100644 index 00000000..d473dcdd --- /dev/null +++ b/src/WixTestTools/BundleRegistration.cs @@ -0,0 +1,145 @@ +// 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 WixTestTools +{ + using System; + using Microsoft.Win32; + + public class BundleRegistration + { + public const string BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY = "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH = "BundleCachePath"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_ADDON_CODE = "BundleAddonCode"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_DETECT_CODE = "BundleDetectCode"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_PATCH_CODE = "BundlePatchCode"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE = "BundleUpgradeCode"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_DISPLAY_NAME = "DisplayName"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION = "BundleVersion"; + public const string BURN_REGISTRATION_REGISTRY_ENGINE_VERSION = "EngineVersion"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_PROVIDER_KEY = "BundleProviderKey"; + public const string BURN_REGISTRATION_REGISTRY_BUNDLE_TAG = "BundleTag"; + public const string REGISTRY_REBOOT_PENDING_FORMAT = "{0}.RebootRequired"; + public const string REGISTRY_BUNDLE_INSTALLED = "Installed"; + public const string REGISTRY_BUNDLE_DISPLAY_ICON = "DisplayIcon"; + public const string REGISTRY_BUNDLE_DISPLAY_VERSION = "DisplayVersion"; + public const string REGISTRY_BUNDLE_ESTIMATED_SIZE = "EstimatedSize"; + public const string REGISTRY_BUNDLE_PUBLISHER = "Publisher"; + public const string REGISTRY_BUNDLE_HELP_LINK = "HelpLink"; + public const string REGISTRY_BUNDLE_HELP_TELEPHONE = "HelpTelephone"; + public const string REGISTRY_BUNDLE_URL_INFO_ABOUT = "URLInfoAbout"; + public const string REGISTRY_BUNDLE_URL_UPDATE_INFO = "URLUpdateInfo"; + public const string REGISTRY_BUNDLE_PARENT_DISPLAY_NAME = "ParentDisplayName"; + public const string REGISTRY_BUNDLE_PARENT_KEY_NAME = "ParentKeyName"; + public const string REGISTRY_BUNDLE_COMMENTS = "Comments"; + public const string REGISTRY_BUNDLE_CONTACT = "Contact"; + public const string REGISTRY_BUNDLE_NO_MODIFY = "NoModify"; + public const string REGISTRY_BUNDLE_MODIFY_PATH = "ModifyPath"; + public const string REGISTRY_BUNDLE_NO_ELEVATE_ON_MODIFY = "NoElevateOnModify"; + public const string REGISTRY_BUNDLE_NO_REMOVE = "NoRemove"; + public const string REGISTRY_BUNDLE_SYSTEM_COMPONENT = "SystemComponent"; + public const string REGISTRY_BUNDLE_QUIET_UNINSTALL_STRING = "QuietUninstallString"; + public const string REGISTRY_BUNDLE_UNINSTALL_STRING = "UninstallString"; + public const string REGISTRY_BUNDLE_RESUME_COMMAND_LINE = "BundleResumeCommandLine"; + public const string REGISTRY_BUNDLE_VERSION_MAJOR = "VersionMajor"; + public const string REGISTRY_BUNDLE_VERSION_MINOR = "VersionMinor"; + + public string[] AddonCodes { get; set; } + + public string CachePath { get; set; } + + public string DisplayName { get; set; } + + public string[] DetectCodes { get; set; } + + public string EngineVersion { get; set; } + + public int? EstimatedSize { get; set; } + + public int? Installed { get; set; } + + public string ModifyPath { get; set; } + + public string[] PatchCodes { get; set; } + + public string ProviderKey { get; set; } + + public string Publisher { get; set; } + + public string QuietUninstallString { get; set; } + + public string QuietUninstallCommand { get; set; } + + public string QuietUninstallCommandArguments { get; set; } + + public string Tag { get; set; } + + public string UninstallCommand { get; set; } + + public string UninstallCommandArguments { get; set; } + + public string UninstallString { get; set; } + + public string[] UpgradeCodes { get; set; } + + public string UrlInfoAbout { get; set; } + + public string UrlUpdateInfo { get; set; } + + public string Version { get; set; } + + public static bool TryGetPerMachineBundleRegistrationById(string bundleId, out BundleRegistration registration) + { + var registrationKeyPath = $"{BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY}\\{bundleId}"; + using var registrationKey = Registry.LocalMachine.OpenSubKey(registrationKeyPath); + var success = registrationKey != null; + registration = success ? GetBundleRegistration(registrationKey) : null; + return success; + } + + private static BundleRegistration GetBundleRegistration(RegistryKey idKey) + { + var registration = new BundleRegistration(); + + registration.AddonCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_ADDON_CODE) as string[]; + registration.CachePath = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH) as string; + registration.DetectCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_DETECT_CODE) as string[]; + registration.PatchCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_PATCH_CODE) as string[]; + registration.ProviderKey = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_PROVIDER_KEY) as string; + registration.Tag = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_TAG) as string; + registration.UpgradeCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE) as string[]; + registration.Version = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION) as string; + registration.DisplayName = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_DISPLAY_NAME) as string; + registration.EngineVersion = idKey.GetValue(BURN_REGISTRATION_REGISTRY_ENGINE_VERSION) as string; + registration.EstimatedSize = idKey.GetValue(REGISTRY_BUNDLE_ESTIMATED_SIZE) as int?; + registration.Installed = idKey.GetValue(REGISTRY_BUNDLE_INSTALLED) as int?; + registration.ModifyPath = idKey.GetValue(REGISTRY_BUNDLE_MODIFY_PATH) as string; + registration.Publisher = idKey.GetValue(REGISTRY_BUNDLE_PUBLISHER) as string; + registration.UrlInfoAbout = idKey.GetValue(REGISTRY_BUNDLE_URL_INFO_ABOUT) as string; + registration.UrlUpdateInfo = idKey.GetValue(REGISTRY_BUNDLE_URL_UPDATE_INFO) as string; + + registration.QuietUninstallString = idKey.GetValue(REGISTRY_BUNDLE_QUIET_UNINSTALL_STRING) as string; + if (!String.IsNullOrEmpty(registration.QuietUninstallString)) + { + var closeQuote = registration.QuietUninstallString.IndexOf("\"", 1); + if (closeQuote > 0) + { + registration.QuietUninstallCommand = registration.QuietUninstallString.Substring(1, closeQuote - 1).Trim(); + registration.QuietUninstallCommandArguments = registration.QuietUninstallString.Substring(closeQuote + 1).Trim(); + } + } + + registration.UninstallString = idKey.GetValue(REGISTRY_BUNDLE_UNINSTALL_STRING) as string; + if (!String.IsNullOrEmpty(registration.UninstallString)) + { + var closeQuote = registration.UninstallString.IndexOf("\"", 1); + if (closeQuote > 0) + { + registration.UninstallCommand = registration.UninstallString.Substring(1, closeQuote - 1).Trim(); + registration.UninstallCommandArguments = registration.UninstallString.Substring(closeQuote + 1).Trim(); + } + } + + return registration; + } + } +} diff --git a/src/WixTestTools/BundleVerifier.cs b/src/WixTestTools/BundleVerifier.cs new file mode 100644 index 00000000..96c86fdf --- /dev/null +++ b/src/WixTestTools/BundleVerifier.cs @@ -0,0 +1,96 @@ +// 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 WixTestTools +{ + using System; + using System.IO; + using System.Linq; + using System.Text; + using Microsoft.Win32; + using WixToolset.Data; + using WixToolset.Data.Symbols; + using Xunit; + + public partial class BundleInstaller + { + public const string FULL_BURN_POLICY_REGISTRY_PATH = "SOFTWARE\\WOW6432Node\\Policies\\WiX\\Burn"; + public const string PACKAGE_CACHE_FOLDER_NAME = "Package Cache"; + + public string BundlePdb { get; } + + private WixBundleSymbol BundleSymbol { get; set; } + + private WixBundleSymbol GetBundleSymbol() + { + if (this.BundleSymbol == null) + { + using var wixOutput = WixOutput.Read(this.BundlePdb); + var intermediate = Intermediate.Load(wixOutput); + var section = intermediate.Sections.Single(); + this.BundleSymbol = section.Symbols.OfType().Single(); + } + + return this.BundleSymbol; + } + + public string GetPackageCachePathForCacheId(string cacheId) + { + using var policyKey = Registry.LocalMachine.OpenSubKey(FULL_BURN_POLICY_REGISTRY_PATH); + var redirectedCachePath = policyKey?.GetValue("PackageCache") as string; + var cachePath = redirectedCachePath ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), PACKAGE_CACHE_FOLDER_NAME); + return Path.Combine(cachePath, cacheId); + } + + public string GetExpectedCachedBundlePath() + { + var bundleSymbol = this.GetBundleSymbol(); + var cachePath = this.GetPackageCachePathForCacheId(bundleSymbol.BundleId); + return Path.Combine(cachePath, Path.GetFileName(this.Bundle)); + } + + public bool TryGetPerMachineRegistration(out BundleRegistration registration) + { + var bundleSymbol = this.GetBundleSymbol(); + var bundleId = bundleSymbol.BundleId; + return BundleRegistration.TryGetPerMachineBundleRegistrationById(bundleId, out registration); + } + + public string VerifyRegisteredAndInPackageCache() + { + Assert.True(this.TryGetPerMachineRegistration(out var registration)); + + Assert.NotNull(registration.CachePath); + Assert.True(File.Exists(registration.CachePath)); + + var expectedCachePath = this.GetExpectedCachedBundlePath(); + Assert.Equal(expectedCachePath, registration.CachePath, StringComparer.OrdinalIgnoreCase); + + return registration.CachePath; + } + + public void VerifyUnregisteredAndRemovedFromPackageCache() + { + var cachedBundlePath = this.GetExpectedCachedBundlePath(); + this.VerifyUnregisteredAndRemovedFromPackageCache(cachedBundlePath); + } + + public void VerifyUnregisteredAndRemovedFromPackageCache(string cachedBundlePath) + { + Assert.False(this.TryGetPerMachineRegistration(out _)); + Assert.False(File.Exists(cachedBundlePath)); + } + + public void RemovePackageFromCache(string packageId) + { + using var wixOutput = WixOutput.Read(this.BundlePdb); + var intermediate = Intermediate.Load(wixOutput); + var section = intermediate.Sections.Single(); + var packageSymbol = section.Symbols.OfType().Single(p => p.Id.Id == packageId); + var cachePath = this.GetPackageCachePathForCacheId(packageSymbol.CacheId); + if (Directory.Exists(cachePath)) + { + Directory.Delete(cachePath, true); + } + } + } +} diff --git a/src/WixTestTools/MSIExec.cs b/src/WixTestTools/MSIExec.cs new file mode 100644 index 00000000..8dce96cf --- /dev/null +++ b/src/WixTestTools/MSIExec.cs @@ -0,0 +1,753 @@ +// 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 WixTestTools +{ + using System; + using System.IO; + using System.Text; + using WixBuildTools.TestSupport; + + public class MSIExec : TestTool + { + /// + /// The expected exit code of the tool + /// + public new MSIExecReturnCode ExpectedExitCode + { + get { return (MSIExecReturnCode)base.ExpectedExitCode; } + set { base.ExpectedExitCode = (int?)value; } + } + + /// + /// Mode of execution (install, uninstall, or repair) + /// + public MSIExecMode ExecutionMode { get; set; } + + /// + /// Path to msi or ProductCode + /// + public string Product { get; set; } + + /// + /// Logging Options + /// + public MSIExecLoggingOptions LoggingOptions { get; set; } + + /// + /// Path to the log file + /// + public string LogFile { get; set; } + + /// + /// Unattended mode - progress bar only + /// + public bool Passive { get; set; } + + /// + /// Quiet mode, no user interaction + /// + public bool Quiet { get; set; } + + /// + /// Sets user interface level + /// + public MSIExecUserInterfaceLevel UserInterfaceLevel { get; set; } + + /// + /// Do not restart after the installation is complete + /// + public bool NoRestart { get; set; } + + /// + /// Prompts the user for restart if necessary + /// + public bool PromptRestart { get; set; } + + /// + /// Always restart the computer after installation + /// + public bool ForceRestart { get; set; } + + /// + /// Other arguments. + /// + public string OtherArguments { get; set; } + + /// + /// Constructor that uses the default location for MSIExec. + /// + public MSIExec() + : this(Environment.SystemDirectory) + { + } + + /// + /// Constructor that accepts a path to the MSIExec location. + /// + /// The directory of MSIExec.exe. + public MSIExec(string toolDirectory) + : base(Path.Combine(toolDirectory, "MSIExec.exe")) + { + this.SetDefaultArguments(); + } + + public override ExternalExecutableResult Run(bool assertOnError) + { + this.Arguments = this.GetArguments(); + return base.Run(assertOnError); + } + + /// + /// Clears all of the assigned arguments and resets them to the default values. + /// + public void SetDefaultArguments() + { + this.ExecutionMode = MSIExecMode.Install; + this.Product = String.Empty; + this.Quiet = true; + this.Passive = false; + this.UserInterfaceLevel = MSIExecUserInterfaceLevel.None; + this.NoRestart = true; + this.ForceRestart = false; + this.PromptRestart = false; + this.LogFile = string.Empty; + this.LoggingOptions = MSIExecLoggingOptions.VOICEWARMUP; + this.OtherArguments = String.Empty; + } + + public string GetArguments() + { + var arguments = new StringBuilder(); + + // quiet + if (this.Quiet) + { + arguments.Append(" /quiet "); + } + + // passive + if (this.Passive) + { + arguments.Append(" /passive "); + } + + // UserInterfaceLevel + switch (this.UserInterfaceLevel) + { + case MSIExecUserInterfaceLevel.None: + arguments.Append(" /qn "); + break; + case MSIExecUserInterfaceLevel.Basic: + arguments.Append(" /qb "); + break; + case MSIExecUserInterfaceLevel.Reduced: + arguments.Append(" /qr "); + break; + case MSIExecUserInterfaceLevel.Full: + arguments.Append(" /qf "); + break; + } + + // NoRestart + if (this.NoRestart) + { + arguments.Append(" /norestart "); + } + + // PromptRestart + if (this.PromptRestart) + { + arguments.Append(" /promptrestart "); + } + + // ForceRestart + if (this.ForceRestart) + { + arguments.Append(" /forcerestart "); + } + + // Logging options + var loggingOptionsString = new StringBuilder(); + if ((this.LoggingOptions & MSIExecLoggingOptions.Status_Messages) == MSIExecLoggingOptions.Status_Messages) + { + loggingOptionsString.Append("i"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.Nonfatal_Warnings) == MSIExecLoggingOptions.Nonfatal_Warnings) + { + loggingOptionsString.Append("w"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.All_Error_Messages) == MSIExecLoggingOptions.All_Error_Messages) + { + loggingOptionsString.Append("e"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.Start_Up_Of_Actions) == MSIExecLoggingOptions.Start_Up_Of_Actions) + { + loggingOptionsString.Append("a"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.Action_Specific_Records) == MSIExecLoggingOptions.Action_Specific_Records) + { + loggingOptionsString.Append("r"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.User_Requests) == MSIExecLoggingOptions.User_Requests) + { + loggingOptionsString.Append("u"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.Initial_UI_Parameters) == MSIExecLoggingOptions.Initial_UI_Parameters) + { + loggingOptionsString.Append("c"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.OutOfMemory_Or_Fatal_Exit_Information) == MSIExecLoggingOptions.OutOfMemory_Or_Fatal_Exit_Information) + { + loggingOptionsString.Append("m"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.OutOfDiskSpace_Messages) == MSIExecLoggingOptions.OutOfDiskSpace_Messages) + { + loggingOptionsString.Append("o"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.Terminal_Properties) == MSIExecLoggingOptions.Terminal_Properties) + { + loggingOptionsString.Append("p"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.Verbose_Output) == MSIExecLoggingOptions.Verbose_Output) + { + loggingOptionsString.Append("v"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.Extra_Debugging_Information) == MSIExecLoggingOptions.Extra_Debugging_Information) + { + loggingOptionsString.Append("x"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.Append_To_Existing_Log_File) == MSIExecLoggingOptions.Append_To_Existing_Log_File) + { + loggingOptionsString.Append("+"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.Flush_Each_line) == MSIExecLoggingOptions.Flush_Each_line) + { + loggingOptionsString.Append("!"); + } + if ((this.LoggingOptions & MSIExecLoggingOptions.Log_All_Information) == MSIExecLoggingOptions.Log_All_Information) + { + loggingOptionsString.Append("*"); + } + + // logfile and logging options + if (0 != loggingOptionsString.Length || !string.IsNullOrEmpty(this.LogFile)) + { + arguments.Append(" /l"); + if (0 != loggingOptionsString.Length) + { + arguments.AppendFormat("{0} ", loggingOptionsString); + } + if (!string.IsNullOrEmpty(this.LogFile)) + { + arguments.AppendFormat(" \"{0}\" ", this.LogFile); + } + } + + // OtherArguments + if (!String.IsNullOrEmpty(this.OtherArguments)) + { + arguments.AppendFormat(" {0} ", this.OtherArguments); + } + + // execution mode + switch (this.ExecutionMode) + { + case MSIExecMode.Install: + arguments.Append(" /package "); + break; + case MSIExecMode.AdministrativeInstall: + arguments.Append(" /a "); + break; + case MSIExecMode.Repair: + arguments.Append(" /f "); + break; + case MSIExecMode.Cleanup: + case MSIExecMode.Uninstall: + arguments.Append(" /uninstall "); + break; + }; + + // product + if (!string.IsNullOrEmpty(this.Product)) + { + arguments.AppendFormat(" \"{0}\" ", this.Product); + } + + return arguments.ToString(); + } + + /// + /// Return codes from an MSI install or uninstall + /// + /// + /// Error codes indicative of success are: + /// ERROR_SUCCESS, ERROR_SUCCESS_REBOOT_INITIATED, and ERROR_SUCCESS_REBOOT_REQUIRED + /// + public enum MSIExecReturnCode + { + /// + /// ERROR_SUCCESS 0 + /// Action completed successfully. + /// + SUCCESS = 0, + + /// + /// ERROR_INVALID_DATA 13 + /// The data is invalid. + /// + ERROR_INVALID_DATA = 13, + + /// + /// ERROR_INVALID_PARAMETER 87 + /// One of the parameters was invalid. + /// + ERROR_INVALID_PARAMETER = 87, + + /// + /// ERROR_CALL_NOT_IMPLEMENTED 120 + /// This value is returned when a custom action attempts to call a function that cannot be called from custom actions. + /// The function returns the value ERROR_CALL_NOT_IMPLEMENTED. Available beginning with Windows Installer version 3.0. + /// + ERROR_CALL_NOT_IMPLEMENTED = 120, + + /// + /// ERROR_APPHELP_BLOCK 1259 + /// If Windows Installer determines a product may be incompatible with the current operating system, + /// it displays a dialog box informing the user and asking whether to try to install anyway. + /// This error code is returned if the user chooses not to try the installation. + /// + ERROR_APPHELP_BLOCK = 1259, + + /// + /// ERROR_INSTALL_SERVICE_FAILURE 1601 + /// The Windows Installer service could not be accessed. + /// Contact your support personnel to verify that the Windows Installer service is properly registered. + /// + ERROR_INSTALL_SERVICE_FAILURE = 1601, + + + /// + /// ERROR_INSTALL_USEREXIT 1602 + /// The user cancels installation. + /// + ERROR_INSTALL_USEREXIT = 1602, + + /// + /// ERROR_INSTALL_FAILURE 1603 + /// A fatal error occurred during installation. + /// + ERROR_INSTALL_FAILURE = 1603, + + /// + /// ERROR_INSTALL_SUSPEND 1604 + /// Installation suspended, incomplete. + /// + ERROR_INSTALL_SUSPEND = 1604, + + /// + /// ERROR_UNKNOWN_PRODUCT 1605 + /// This action is only valid for products that are currently installed. + /// + ERROR_UNKNOWN_PRODUCT = 1605, + + /// + /// ERROR_UNKNOWN_FEATURE 1606 + /// The feature identifier is not registered. + /// + ERROR_UNKNOWN_FEATURE = 1606, + + /// + /// ERROR_UNKNOWN_COMPONENT 1607 + /// The component identifier is not registered. + /// + ERROR_UNKNOWN_COMPONENT = 1607, + + /// + /// ERROR_UNKNOWN_PROPERTY 1608 + /// This is an unknown property. + /// + ERROR_UNKNOWN_PROPERTY = 1608, + + /// + /// ERROR_INVALID_HANDLE_STATE 1609 + /// The handle is in an invalid state. + /// + ERROR_INVALID_HANDLE_STATE = 1609, + + /// + /// ERROR_BAD_CONFIGURATION 1610 + /// The configuration data for this product is corrupt. Contact your support personnel. + /// + ERROR_BAD_CONFIGURATION = 1610, + + /// + /// ERROR_INDEX_ABSENT 1611 + /// The component qualifier not present. + /// + ERROR_INDEX_ABSENT = 1611, + + /// ERROR_INSTALL_SOURCE_ABSENT 1612 + /// The installation source for this product is not available. + /// Verify that the source exists and that you can access it. + /// + ERROR_INSTALL_SOURCE_ABSENT = 1612, + + /// + /// ERROR_INSTALL_PACKAGE_VERSION 1613 + /// This installation package cannot be installed by the Windows Installer service. + /// You must install a Windows service pack that contains a newer version of the Windows Installer service. + /// + ERROR_INSTALL_PACKAGE_VERSION = 1613, + + /// + /// ERROR_PRODUCT_UNINSTALLED 1614 + /// The product is uninstalled. + /// + ERROR_PRODUCT_UNINSTALLED = 1614, + + /// + /// ERROR_BAD_QUERY_SYNTAX 1615 + /// The SQL query syntax is invalid or unsupported. + /// + ERROR_BAD_QUERY_SYNTAX = 1615, + + /// + /// ERROR_INVALID_FIELD 1616 + /// The record field does not exist. + /// + ERROR_INVALID_FIELD = 1616, + + /// + /// ERROR_INSTALL_ALREADY_RUNNING 1618 + /// Another installation is already in progress. Complete that installation before proceeding with this install. + /// For information about the mutex, see _MSIExecute Mutex. + /// + ERROR_INSTALL_ALREADY_RUNNING = 1618, + + /// + /// ERROR_INSTALL_PACKAGE_OPEN_FAILED 1619 + /// This installation package could not be opened. Verify that the package exists and is accessible, or contact the + /// application vendor to verify that this is a valid Windows Installer package. + /// + ERROR_INSTALL_PACKAGE_OPEN_FAILED = 1619, + + + /// + /// ERROR_INSTALL_PACKAGE_INVALID 1620 + /// This installation package could not be opened. + /// Contact the application vendor to verify that this is a valid Windows Installer package. + /// + ERROR_INSTALL_PACKAGE_INVALID = 1620, + + /// + /// ERROR_INSTALL_UI_FAILURE 1621 + /// There was an error starting the Windows Installer service user interface. + /// Contact your support personnel. + /// + ERROR_INSTALL_UI_FAILURE = 1621, + + /// + /// ERROR_INSTALL_LOG_FAILURE 1622 + /// There was an error opening installation log file. + /// Verify that the specified log file location exists and is writable. + /// + ERROR_INSTALL_LOG_FAILURE = 1622, + + /// + /// ERROR_INSTALL_LANGUAGE_UNSUPPORTED 1623 + /// This language of this installation package is not supported by your system. + /// + ERROR_INSTALL_LANGUAGE_UNSUPPORTED = 1623, + + /// + /// ERROR_INSTALL_TRANSFORM_FAILURE 1624 + /// There was an error applying transforms. + /// Verify that the specified transform paths are valid. + /// + ERROR_INSTALL_TRANSFORM_FAILURE = 1624, + + + /// + /// ERROR_INSTALL_PACKAGE_REJECTED 1625 + /// This installation is forbidden by system policy. + /// Contact your system administrator. + /// + ERROR_INSTALL_PACKAGE_REJECTED = 1625, + + /// + /// ERROR_FUNCTION_NOT_CALLED 1626 + /// The function could not be executed. + /// + ERROR_FUNCTION_NOT_CALLED = 1626, + + /// + /// ERROR_FUNCTION_FAILED 1627 + /// The function failed during execution. + /// + ERROR_FUNCTION_FAILED = 1627, + + /// + /// ERROR_INVALID_TABLE 1628 + /// An invalid or unknown table was specified. + /// + ERROR_INVALID_TABLE = 1628, + + /// + /// ERROR_DATATYPE_MISMATCH 1629 + /// The data supplied is the wrong type. + /// + ERROR_DATATYPE_MISMATCH = 1629, + + /// + /// ERROR_UNSUPPORTED_TYPE 1630 + /// Data of this type is not supported. + /// + ERROR_UNSUPPORTED_TYPE = 1630, + + /// + /// ERROR_CREATE_FAILED 1631 + /// The Windows Installer service failed to start. + /// Contact your support personnel. + /// + ERROR_CREATE_FAILED = 1631, + + /// + /// ERROR_INSTALL_TEMP_UNWRITABLE 1632 + /// The Temp folder is either full or inaccessible. + /// Verify that the Temp folder exists and that you can write to it. + /// + ERROR_INSTALL_TEMP_UNWRITABLE = 1632, + + /// + /// ERROR_INSTALL_PLATFORM_UNSUPPORTED 1633 + /// This installation package is not supported on this platform. Contact your application vendor. + ERROR_INSTALL_PLATFORM_UNSUPPORTED = 1633, + + /// + /// ERROR_INSTALL_NOTUSED 1634 + /// Component is not used on this machine. + /// + ERROR_INSTALL_NOTUSED = 1634, + + /// + /// ERROR_PATCH_PACKAGE_OPEN_FAILED 1635 + /// This patch package could not be opened. Verify that the patch package exists and is accessible, + /// or contact the application vendor to verify that this is a valid Windows Installer patch package. + /// + ERROR_PATCH_PACKAGE_OPEN_FAILED = 1635, + + /// + /// ERROR_PATCH_PACKAGE_INVALID 1636 + /// This patch package could not be opened. + /// Contact the application vendor to verify that this is a valid Windows Installer patch package. + /// + ERROR_PATCH_PACKAGE_INVALID = 1636, + + /// + /// ERROR_PATCH_PACKAGE_UNSUPPORTED 1637 + /// This patch package cannot be processed by the Windows Installer service. + /// You must install a Windows service pack that contains a newer version of the Windows Installer service. + /// + ERROR_PATCH_PACKAGE_UNSUPPORTED = 1637, + + /// + /// ERROR_PRODUCT_VERSION 1638 + /// Another version of this product is already installed. + /// Installation of this version cannot continue. To configure or remove the existing version of this product, + /// use Add/Remove Programs in Control Panel. + /// + ERROR_PRODUCT_VERSION = 1638, + + /// + /// ERROR_INVALID_COMMAND_LINE 1639 + /// Invalid command line argument. + /// Consult the Windows Installer SDK for detailed command-line help. + /// + ERROR_INVALID_COMMAND_LINE = 1639, + + /// + /// ERROR_INSTALL_REMOTE_DISALLOWED 1640 + /// The current user is not permitted to perform installations from a client session of a server running the + /// Terminal Server role service. + /// + ERROR_INSTALL_REMOTE_DISALLOWED = 1640, + + /// + /// ERROR_SUCCESS_REBOOT_INITIATED 1641 + /// The installer has initiated a restart. + /// This message is indicative of a success. + /// + ERROR_SUCCESS_REBOOT_INITIATED = 1641, + + /// + /// ERROR_PATCH_TARGET_NOT_FOUND 1642 + /// The installer cannot install the upgrade patch because the program being upgraded may be missing or the + /// upgrade patch updates a different version of the program. + /// Verify that the program to be upgraded exists on your computer and that you have the correct upgrade patch. + /// + ERROR_PATCH_TARGET_NOT_FOUND = 1642, + + /// + /// ERROR_PATCH_PACKAGE_REJECTED 1643 + /// The patch package is not permitted by system policy. + /// + ERROR_PATCH_PACKAGE_REJECTED = 1643, + + /// + /// ERROR_INSTALL_TRANSFORM_REJECTED 1644 + /// One or more customizations are not permitted by system policy. + /// + ERROR_INSTALL_TRANSFORM_REJECTED = 1644, + + /// + /// ERROR_INSTALL_REMOTE_PROHIBITED 1645 + /// Windows Installer does not permit installation from a Remote Desktop Connection. + /// + ERROR_INSTALL_REMOTE_PROHIBITED = 1645, + + /// + /// ERROR_PATCH_REMOVAL_UNSUPPORTED 1646 + /// The patch package is not a removable patch package. Available beginning with Windows Installer version 3.0. + /// + ERROR_PATCH_REMOVAL_UNSUPPORTED = 1646, + + /// + /// ERROR_UNKNOWN_PATCH 1647 + /// The patch is not applied to this product. Available beginning with Windows Installer version 3.0. + /// + ERROR_UNKNOWN_PATCH = 1647, + + /// + /// ERROR_PATCH_NO_SEQUENCE 1648 + /// No valid sequence could be found for the set of patches. Available beginning with Windows Installer version 3.0. + /// + ERROR_PATCH_NO_SEQUENCE = 1648, + + /// + /// ERROR_PATCH_REMOVAL_DISALLOWED 1649 + /// Patch removal was disallowed by policy. Available beginning with Windows Installer version 3.0. + ERROR_PATCH_REMOVAL_DISALLOWED = 1649, + + /// + /// ERROR_INVALID_PATCH_XML = 1650 + /// The XML patch data is invalid. Available beginning with Windows Installer version 3.0. + /// + ERROR_INVALID_PATCH_XML = 1650, + + /// + /// ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT 1651 + /// Administrative user failed to apply patch for a per-user managed or a per-machine application that is in advertise state. + /// Available beginning with Windows Installer version 3.0. + ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT = 1651, + + /// + /// ERROR_INSTALL_SERVICE_SAFEBOOT 1652 + /// Windows Installer is not accessible when the computer is in Safe Mode. + /// Exit Safe Mode and try again or try using System Restore to return your computer to a previous state. + /// Available beginning with Windows Installer version 4.0. + /// + ERROR_INSTALL_SERVICE_SAFEBOOT = 1652, + + /// + /// ERROR_ROLLBACK_DISABLED 1653 + /// Could not perform a multiple-package transaction because rollback has been disabled. + /// Multiple-Package Installations cannot run if rollback is disabled. Available beginning with Windows Installer version 4.5. + /// + ERROR_ROLLBACK_DISABLED = 1653, + + /// + /// ERROR_SUCCESS_REBOOT_REQUIRED 3010 + /// A restart is required to complete the install. This message is indicative of a success. + /// This does not include installs where the ForceReboot action is run. + /// + ERROR_SUCCESS_REBOOT_REQUIRED = 3010 + } + + /// + /// Modes of operations for MSIExec; install, administrator install, uninstall .. etc + /// + public enum MSIExecMode + { + /// + /// Installs or configures a product + /// + Install = 0, + + /// + /// Administrative install - Installs a product on the network + /// + AdministrativeInstall, + + /// + /// Uninstalls the product + /// + Uninstall, + + /// + /// Repairs a product + /// + Repair, + + /// + /// Modifies a product + /// + Modify, + + /// + /// Uninstalls the product as part of cleanup + /// + Cleanup, + } + + /// + /// User interfave levels + /// + public enum MSIExecUserInterfaceLevel + { + /// + /// No UI + /// + None = 0, + + /// + /// Basic UI + /// + Basic, + + /// + /// Reduced UI + /// + Reduced, + + /// + /// Full UI (default) + /// + Full + } + + /// + /// Logging options + /// + [Flags] + public enum MSIExecLoggingOptions + { + Status_Messages = 0x0001, + Nonfatal_Warnings = 0x0002, + All_Error_Messages = 0x0004, + Start_Up_Of_Actions = 0x0008, + Action_Specific_Records = 0x0010, + User_Requests = 0x0020, + Initial_UI_Parameters = 0x0040, + OutOfMemory_Or_Fatal_Exit_Information = 0x0080, + OutOfDiskSpace_Messages = 0x0100, + Terminal_Properties = 0x0200, + Verbose_Output = 0x0400, + Append_To_Existing_Log_File = 0x0800, + + Flush_Each_line = 0x1000, + Extra_Debugging_Information = 0x2000, + Log_All_Information = 0x4000, + VOICEWARMUP = 0x0FFF + } + } +} diff --git a/src/WixTestTools/MsiUtilities.cs b/src/WixTestTools/MsiUtilities.cs new file mode 100644 index 00000000..2a848938 --- /dev/null +++ b/src/WixTestTools/MsiUtilities.cs @@ -0,0 +1,27 @@ +// 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 WixTestTools +{ + using WixToolset.Dtf.WindowsInstaller; + + public class MsiUtilities + { + /// + /// Return true if it finds the given productcode in system otherwise it returns false + /// + /// + /// + public static bool IsProductInstalled(string prodCode) + { + //look in all user's products (both per-machine and per-user) + foreach (ProductInstallation product in ProductInstallation.GetProducts(null, "s-1-1-0", UserContexts.All)) + { + if (product.ProductCode == prodCode) + { + return true; + } + } + return false; + } + } +} diff --git a/src/WixTestTools/PackageInstaller.cs b/src/WixTestTools/PackageInstaller.cs new file mode 100644 index 00000000..86376b9f --- /dev/null +++ b/src/WixTestTools/PackageInstaller.cs @@ -0,0 +1,90 @@ +// 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 WixTestTools +{ + using System; + using System.IO; + using static WixTestTools.MSIExec; + + public partial class PackageInstaller : IDisposable + { + public PackageInstaller(WixTestContext testContext, string filename) + { + this.Package = Path.Combine(testContext.TestDataFolder, $"{filename}.msi"); + this.PackagePdb = Path.Combine(testContext.TestDataFolder, $"{filename}.wixpdb"); + this.TestContext = testContext; + } + + public string Package { get; } + + private WixTestContext TestContext { get; } + + public string TestGroupName => this.TestContext.TestGroupName; + + public string TestName => this.TestContext.TestName; + + /// + /// Installs a .msi file + /// + /// Expected exit code + /// Other arguments to pass to MSIExec. + /// MSIExec log File + public string InstallProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments) + { + return this.RunMSIExec(MSIExecMode.Install, otherArguments, expectedExitCode); + } + + /// + /// Uninstalls a .msi file + /// + /// Expected exit code + /// Other arguments to pass to MSIExec. + /// MSIExec log File + public string UninstallProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments) + { + return this.RunMSIExec(MSIExecMode.Uninstall, otherArguments, expectedExitCode); + } + + /// + /// Repairs a .msi file + /// + /// Expected exit code + /// Other arguments to pass to msiexe.exe. + /// MSIExec log File + public string RepairProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments) + { + return this.RunMSIExec(MSIExecMode.Repair, otherArguments, expectedExitCode); + } + + /// + /// Executes MSIExec on a .msi file + /// + /// Mode of execution for MSIExec + /// Other arguments to pass to MSIExec. + /// Expected exit code + /// MSIExec exit code + private string RunMSIExec(MSIExecMode mode, string[] otherArguments, MSIExecReturnCode expectedExitCode, bool assertOnError = true) + { + // Generate the log file name. + var logFile = Path.Combine(Path.GetTempPath(), String.Format("{0}_{1}_{2:yyyyMMddhhmmss}_{4}_{3}.log", this.TestGroupName, this.TestName, DateTime.UtcNow, Path.GetFileNameWithoutExtension(this.Package), mode)); + + var msiexec = new MSIExec + { + Product = this.Package, + ExecutionMode = mode, + OtherArguments = null != otherArguments ? String.Join(" ", otherArguments) : null, + ExpectedExitCode = expectedExitCode, + LogFile = logFile, + }; + + msiexec.Run(assertOnError); + return msiexec.LogFile; + } + + public void Dispose() + { + string[] args = { "IGNOREDEPENDENCIES=ALL", "WIXFAILWHENDEFERRED=0" }; + this.RunMSIExec(MSIExecMode.Cleanup, args, MSIExecReturnCode.SUCCESS, assertOnError: false); + } + } +} diff --git a/src/WixTestTools/PackageVerifier.cs b/src/WixTestTools/PackageVerifier.cs new file mode 100644 index 00000000..77946c91 --- /dev/null +++ b/src/WixTestTools/PackageVerifier.cs @@ -0,0 +1,77 @@ +// 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 WixTestTools +{ + using System; + using System.IO; + using System.Linq; + using WixToolset.Data; + using WixToolset.Data.WindowsInstaller; + using WixToolset.Data.WindowsInstaller.Rows; + using Xunit; + + public partial class PackageInstaller + { + public string PackagePdb { get; } + + private WindowsInstallerData WiData { get; set; } + + public string GetInstalledFilePath(string filename) + { + return this.TestContext.GetTestInstallFolder(Path.Combine(this.GetInstallFolderName(), filename)); + } + + private WindowsInstallerData GetWindowsInstallerData() + { + if (this.WiData == null) + { + using var wixOutput = WixOutput.Read(this.PackagePdb); + this.WiData = WindowsInstallerData.Load(wixOutput); + } + + return this.WiData; + } + + public string GetInstallFolderName() + { + var wiData = this.GetWindowsInstallerData(); + var row = wiData.Tables["Directory"].Rows.Single(r => r.FieldAsString(0) == "INSTALLFOLDER"); + var value = row.FieldAsString(2); + var longNameIndex = value.IndexOf('|') + 1; + if (longNameIndex > 0) + { + return value.Substring(longNameIndex); + } + return value; + } + + public string GetProperty(string name) + { + var wiData = this.GetWindowsInstallerData(); + var row = wiData.Tables["Property"].Rows.Cast().Single(r => r.Property == name); + return row.Value; + } + + public void VerifyInstalled(bool installed) + { + var productCode = this.GetProperty("ProductCode"); + Assert.Equal(installed, MsiUtilities.IsProductInstalled(productCode)); + } + + public void VerifyTestRegistryRootDeleted() + { + using var testRegistryRoot = this.TestContext.GetTestRegistryRoot(); + Assert.Null(testRegistryRoot); + } + + public void VerifyTestRegistryValue(string name, string expectedValue) + { + using (var root = this.TestContext.GetTestRegistryRoot()) + { + Assert.NotNull(root); + var actualValue = root.GetValue(name) as string; + Assert.Equal(expectedValue, actualValue); + } + } + } +} diff --git a/src/WixTestTools/TestTool.cs b/src/WixTestTools/TestTool.cs new file mode 100644 index 00000000..be5fde42 --- /dev/null +++ b/src/WixTestTools/TestTool.cs @@ -0,0 +1,245 @@ +// 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 WixTestTools +{ + using System; + using System.Collections.Generic; + using System.Text; + using System.Text.RegularExpressions; + using WixBuildTools.TestSupport; + using Xunit; + + public class TestTool : ExternalExecutable + { + /// + /// Constructor for a TestTool + /// + public TestTool() + : this(null) + { + } + + /// + /// Constructor for a TestTool + /// + /// The full path to the tool. Eg. c:\bin\candle.exe + public TestTool(string toolFile) + : base(toolFile) + { + this.PrintOutputToConsole = true; + } + + /// + /// The arguments to pass to the tool + /// + public virtual string Arguments { get; set; } + + /// + /// Stores the errors that occurred when a run was checked against its expected results + /// + public List Errors { get; set; } + + /// + /// A list of Regex's that are expected to match stderr + /// + public List ExpectedErrorRegexs { get; set; } = new List(); + + /// + /// The expected error strings to stderr + /// + public List ExpectedErrorStrings { get; set; } = new List(); + + /// + /// The expected exit code of the tool + /// + public int? ExpectedExitCode { get; set; } + + /// + /// A list of Regex's that are expected to match stdout + /// + public List ExpectedOutputRegexs { get; set; } = new List(); + + /// + /// The expected output strings to stdout + /// + public List ExpectedOutputStrings { get; set; } = new List(); + + /// + /// Print output from the tool execution to the console + /// + public bool PrintOutputToConsole { get; set; } + + /// + /// The working directory of the tool + /// + public string WorkingDirectory { get; set; } + + /// + /// Print the errors from the last run + /// + public void PrintErrors() + { + if (null != this.Errors) + { + Console.WriteLine("Errors:"); + + foreach (string error in this.Errors) + { + Console.WriteLine(error); + } + } + } + + /// + /// Run the tool + /// + /// The results of the run + public ExternalExecutableResult Run() + { + return this.Run(true); + } + + /// + /// Run the tool + /// + /// Throw an exception if the expected results don't match the actual results + /// Thrown when the expected results don't match the actual results + /// The results of the run + public virtual ExternalExecutableResult Run(bool assertOnError) + { + var result = this.Run(this.Arguments, workingDirectory: this.WorkingDirectory ?? String.Empty); + + if (this.PrintOutputToConsole) + { + Console.WriteLine(FormatResult(result)); + } + + this.Errors = this.CheckResult(result); + + if (assertOnError && 0 < this.Errors.Count) + { + if (this.PrintOutputToConsole) + { + this.PrintErrors(); + } + + Assert.Empty(this.Errors); + } + + return result; + } + + /// + /// Checks that the result from a run matches the expected results + /// + /// A result from a run + /// A list of errors + public virtual List CheckResult(ExternalExecutableResult result) + { + List errors = new List(); + + // Verify that the expected return code matched the actual return code + if (null != this.ExpectedExitCode && this.ExpectedExitCode != result.ExitCode) + { + errors.Add(String.Format("Expected exit code {0} did not match actual exit code {1}", this.ExpectedExitCode, result.ExitCode)); + } + + var standardErrorString = string.Join(Environment.NewLine, result.StandardError); + + // Verify that the expected error string are in stderr + if (null != this.ExpectedErrorStrings) + { + foreach (string expectedString in this.ExpectedErrorStrings) + { + if (!standardErrorString.Contains(expectedString)) + { + errors.Add(String.Format("The text '{0}' was not found in stderr", expectedString)); + } + } + } + + var standardOutputString = string.Join(Environment.NewLine, result.StandardOutput); + + // Verify that the expected output string are in stdout + if (null != this.ExpectedOutputStrings) + { + foreach (string expectedString in this.ExpectedOutputStrings) + { + if (!standardOutputString.Contains(expectedString)) + { + errors.Add(String.Format("The text '{0}' was not found in stdout", expectedString)); + } + } + } + + // Verify that the expected regular expressions match stderr + if (null != this.ExpectedOutputRegexs) + { + foreach (Regex expectedRegex in this.ExpectedOutputRegexs) + { + if (!expectedRegex.IsMatch(standardOutputString)) + { + errors.Add(String.Format("Regex {0} did not match stdout", expectedRegex.ToString())); + } + } + } + + // Verify that the expected regular expressions match stdout + if (null != this.ExpectedErrorRegexs) + { + foreach (Regex expectedRegex in this.ExpectedErrorRegexs) + { + if (!expectedRegex.IsMatch(standardErrorString)) + { + errors.Add(String.Format("Regex {0} did not match stderr", expectedRegex.ToString())); + } + } + } + + return errors; + } + + /// + /// Clears all of the expected results and resets them to the default values + /// + public virtual void SetDefaultExpectedResults() + { + this.ExpectedErrorRegexs = new List(); + this.ExpectedErrorStrings = new List(); + this.ExpectedExitCode = null; + this.ExpectedOutputRegexs = new List(); + this.ExpectedOutputStrings = new List(); + } + + /// + /// Returns a string with data contained in the result. + /// + /// A string + private static string FormatResult(ExternalExecutableResult result) + { + var returnValue = new StringBuilder(); + returnValue.AppendLine(); + returnValue.AppendLine("----------------"); + returnValue.AppendLine("Tool run result:"); + returnValue.AppendLine("----------------"); + returnValue.AppendLine("Command:"); + returnValue.AppendLine($"\"{result.StartInfo.FileName}\" {result.StartInfo.Arguments}"); + returnValue.AppendLine(); + returnValue.AppendLine("Standard Output:"); + foreach (var line in result.StandardOutput ?? new string[0]) + { + returnValue.AppendLine(line); + } + returnValue.AppendLine("Standard Error:"); + foreach (var line in result.StandardError ?? new string[0]) + { + returnValue.AppendLine(line); + } + returnValue.AppendLine("Exit Code:"); + returnValue.AppendLine(Convert.ToString(result.ExitCode)); + returnValue.AppendLine("----------------"); + + return returnValue.ToString(); + } + } +} diff --git a/src/WixTestTools/WixTestBase.cs b/src/WixTestTools/WixTestBase.cs new file mode 100644 index 00000000..bc050135 --- /dev/null +++ b/src/WixTestTools/WixTestBase.cs @@ -0,0 +1,19 @@ +// 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 WixTestTools +{ + using Xunit.Abstractions; + + public abstract class WixTestBase + { + protected WixTestBase(ITestOutputHelper testOutputHelper) + { + this.TestContext = new WixTestContext(testOutputHelper); + } + + /// + /// The test context for the current test. + /// + public WixTestContext TestContext { get; } + } +} diff --git a/src/WixTestTools/WixTestContext.cs b/src/WixTestTools/WixTestContext.cs new file mode 100644 index 00000000..c00f5723 --- /dev/null +++ b/src/WixTestTools/WixTestContext.cs @@ -0,0 +1,73 @@ +// 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 WixTestTools +{ + using System; + using System.IO; + using System.Linq; + using System.Reflection; + using Microsoft.Win32; + using WixBuildTools.TestSupport; + using Xunit.Abstractions; + + public class WixTestContext + { + static readonly string RootDataPath = Path.GetFullPath(TestData.Get("TestData")); + + public WixTestContext(ITestOutputHelper testOutputHelper) + { + var test = GetTest(testOutputHelper); + var splitClassName = test.TestCase.TestMethod.TestClass.Class.Name.Split('.'); + + this.TestGroupName = splitClassName.Last(); + this.TestName = test.TestCase.TestMethod.Method.Name; + + this.TestDataFolder = Path.Combine(RootDataPath, this.TestGroupName); + } + + public string TestDataFolder { get; } + + /// + /// Gets the name of the current test group. + /// + public string TestGroupName { get; } + + public string TestName { get; } + + /// + /// Gets the test install directory for the current test. + /// + /// Additional subdirectories under the test install directory. + /// Full path to the test install directory. + /// + /// The package or bundle must install into [ProgramFilesFolder]\~Test WiX\[TestGroupName]\([Additional]). + /// + public string GetTestInstallFolder(string additionalPath = null) + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "~Test WiX", this.TestGroupName, additionalPath ?? String.Empty); + } + + /// + /// Gets the test registry key for the current test. + /// + /// Additional subkeys under the test registry key. + /// Full path to the test registry key. + /// + /// The package must write into HKLM\Software\WiX\Tests\[TestGroupName]\([Additional]). + /// + public RegistryKey GetTestRegistryRoot(string additionalPath = null) + { + var key = String.Format(@"Software\WOW6432Node\WiX\Tests\{0}\{1}", this.TestGroupName, additionalPath ?? String.Empty); + return Registry.LocalMachine.OpenSubKey(key, true); + } + + private static ITest GetTest(ITestOutputHelper output) + { + // https://github.com/xunit/xunit/issues/416#issuecomment-378512739 + var type = output.GetType(); + var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); + var test = (ITest)testMember.GetValue(output); + return test; + } + } +} diff --git a/src/WixTestTools/WixTestTools.csproj b/src/WixTestTools/WixTestTools.csproj new file mode 100644 index 00000000..0a2db104 --- /dev/null +++ b/src/WixTestTools/WixTestTools.csproj @@ -0,0 +1,21 @@ + + + + + + netcoreapp3.1 + x64 + + + + + + + + + + + + + + diff --git a/src/WixToolsetTest.BurnE2E/BundleInstaller.cs b/src/WixToolsetTest.BurnE2E/BundleInstaller.cs deleted file mode 100644 index 923618b9..00000000 --- a/src/WixToolsetTest.BurnE2E/BundleInstaller.cs +++ /dev/null @@ -1,141 +0,0 @@ -// 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.IO; - using System.Text; - - public partial class BundleInstaller : IDisposable - { - public BundleInstaller(WixTestContext testContext, string name) - { - this.Bundle = Path.Combine(testContext.TestDataFolder, $"{name}.exe"); - this.BundlePdb = Path.Combine(testContext.TestDataFolder, $"{name}.wixpdb"); - this.TestGroupName = testContext.TestGroupName; - this.TestName = testContext.TestName; - } - - public string Bundle { get; } - - public string TestGroupName { get; } - - public string TestName { get; } - - /// - /// Installs the bundle with optional arguments. - /// - /// Expected exit code, defaults to success. - /// Optional arguments to pass to the tool. - /// Path to the generated log file. - public string Install(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) - { - return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Install, arguments); - } - - /// - /// Modify the bundle with optional arguments. - /// - /// Expected exit code, defaults to success. - /// Optional arguments to pass to the tool. - /// Path to the generated log file. - public string Modify(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) - { - return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Modify, arguments); - } - - /// - /// Repairs the bundle with optional arguments. - /// - /// Expected exit code, defaults to success. - /// Optional arguments to pass to the tool. - /// Path to the generated log file. - public string Repair(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) - { - return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Repair, arguments); - } - - /// - /// Uninstalls the bundle with optional arguments. - /// - /// Expected exit code, defaults to success. - /// Optional arguments to pass to the tool. - /// Path to the generated log file. - public string Uninstall(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) - { - return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Uninstall, arguments); - } - - /// - /// Uninstalls the bundle at the given path with optional arguments. - /// - /// This should be the bundle in the package cache. - /// Expected exit code, defaults to success. - /// Optional arguments to pass to the tool. - /// Path to the generated log file. - public string Uninstall(string bundlePath, int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments) - { - return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Uninstall, arguments, bundlePath: bundlePath); - } - - /// - /// Executes the bundle with optional arguments. - /// - /// Expected exit code. - /// Install mode. - /// Optional arguments to pass to the tool. - /// Path to the generated log file. - private string RunBundleWithArguments(int expectedExitCode, MSIExec.MSIExecMode mode, string[] arguments, bool assertOnError = true, string bundlePath = null) - { - TestTool bundle = new TestTool(bundlePath ?? this.Bundle); - var sb = new StringBuilder(); - - // Be sure to run silent. - 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)); - sb.AppendFormat(" -log \"{0}\"", logFile); - - // Set operation. - switch (mode) - { - case MSIExec.MSIExecMode.Modify: - sb.Append(" -modify"); - break; - - case MSIExec.MSIExecMode.Repair: - sb.Append(" -repair"); - break; - - case MSIExec.MSIExecMode.Cleanup: - case MSIExec.MSIExecMode.Uninstall: - sb.Append(" -uninstall"); - break; - } - - // Add additional arguments. - if (null != arguments) - { - sb.Append(" "); - sb.Append(String.Join(" ", arguments)); - } - - // Set the arguments. - bundle.Arguments = sb.ToString(); - - // Run the tool and assert the expected code. - bundle.ExpectedExitCode = expectedExitCode; - bundle.Run(assertOnError); - - // Return the log file name. - return logFile; - } - - public void Dispose() - { - string[] args = { "-burn.ignoredependencies=ALL" }; - this.RunBundleWithArguments((int)MSIExec.MSIExecReturnCode.SUCCESS, MSIExec.MSIExecMode.Cleanup, args, assertOnError: false); - } - } -} diff --git a/src/WixToolsetTest.BurnE2E/BundleRegistration.cs b/src/WixToolsetTest.BurnE2E/BundleRegistration.cs deleted file mode 100644 index 6d20a0b2..00000000 --- a/src/WixToolsetTest.BurnE2E/BundleRegistration.cs +++ /dev/null @@ -1,145 +0,0 @@ -// 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 Microsoft.Win32; - - public class BundleRegistration - { - public const string BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY = "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH = "BundleCachePath"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_ADDON_CODE = "BundleAddonCode"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_DETECT_CODE = "BundleDetectCode"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_PATCH_CODE = "BundlePatchCode"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE = "BundleUpgradeCode"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_DISPLAY_NAME = "DisplayName"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION = "BundleVersion"; - public const string BURN_REGISTRATION_REGISTRY_ENGINE_VERSION = "EngineVersion"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_PROVIDER_KEY = "BundleProviderKey"; - public const string BURN_REGISTRATION_REGISTRY_BUNDLE_TAG = "BundleTag"; - public const string REGISTRY_REBOOT_PENDING_FORMAT = "{0}.RebootRequired"; - public const string REGISTRY_BUNDLE_INSTALLED = "Installed"; - public const string REGISTRY_BUNDLE_DISPLAY_ICON = "DisplayIcon"; - public const string REGISTRY_BUNDLE_DISPLAY_VERSION = "DisplayVersion"; - public const string REGISTRY_BUNDLE_ESTIMATED_SIZE = "EstimatedSize"; - public const string REGISTRY_BUNDLE_PUBLISHER = "Publisher"; - public const string REGISTRY_BUNDLE_HELP_LINK = "HelpLink"; - public const string REGISTRY_BUNDLE_HELP_TELEPHONE = "HelpTelephone"; - public const string REGISTRY_BUNDLE_URL_INFO_ABOUT = "URLInfoAbout"; - public const string REGISTRY_BUNDLE_URL_UPDATE_INFO = "URLUpdateInfo"; - public const string REGISTRY_BUNDLE_PARENT_DISPLAY_NAME = "ParentDisplayName"; - public const string REGISTRY_BUNDLE_PARENT_KEY_NAME = "ParentKeyName"; - public const string REGISTRY_BUNDLE_COMMENTS = "Comments"; - public const string REGISTRY_BUNDLE_CONTACT = "Contact"; - public const string REGISTRY_BUNDLE_NO_MODIFY = "NoModify"; - public const string REGISTRY_BUNDLE_MODIFY_PATH = "ModifyPath"; - public const string REGISTRY_BUNDLE_NO_ELEVATE_ON_MODIFY = "NoElevateOnModify"; - public const string REGISTRY_BUNDLE_NO_REMOVE = "NoRemove"; - public const string REGISTRY_BUNDLE_SYSTEM_COMPONENT = "SystemComponent"; - public const string REGISTRY_BUNDLE_QUIET_UNINSTALL_STRING = "QuietUninstallString"; - public const string REGISTRY_BUNDLE_UNINSTALL_STRING = "UninstallString"; - public const string REGISTRY_BUNDLE_RESUME_COMMAND_LINE = "BundleResumeCommandLine"; - public const string REGISTRY_BUNDLE_VERSION_MAJOR = "VersionMajor"; - public const string REGISTRY_BUNDLE_VERSION_MINOR = "VersionMinor"; - - public string[] AddonCodes { get; set; } - - public string CachePath { get; set; } - - public string DisplayName { get; set; } - - public string[] DetectCodes { get; set; } - - public string EngineVersion { get; set; } - - public int? EstimatedSize { get; set; } - - public int? Installed { get; set; } - - public string ModifyPath { get; set; } - - public string[] PatchCodes { get; set; } - - public string ProviderKey { get; set; } - - public string Publisher { get; set; } - - public string QuietUninstallString { get; set; } - - public string QuietUninstallCommand { get; set; } - - public string QuietUninstallCommandArguments { get; set; } - - public string Tag { get; set; } - - public string UninstallCommand { get; set; } - - public string UninstallCommandArguments { get; set; } - - public string UninstallString { get; set; } - - public string[] UpgradeCodes { get; set; } - - public string UrlInfoAbout { get; set; } - - public string UrlUpdateInfo { get; set; } - - public string Version { get; set; } - - public static bool TryGetPerMachineBundleRegistrationById(string bundleId, out BundleRegistration registration) - { - var registrationKeyPath = $"{BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY}\\{bundleId}"; - using var registrationKey = Registry.LocalMachine.OpenSubKey(registrationKeyPath); - var success = registrationKey != null; - registration = success ? GetBundleRegistration(registrationKey) : null; - return success; - } - - private static BundleRegistration GetBundleRegistration(RegistryKey idKey) - { - var registration = new BundleRegistration(); - - registration.AddonCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_ADDON_CODE) as string[]; - registration.CachePath = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH) as string; - registration.DetectCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_DETECT_CODE) as string[]; - registration.PatchCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_PATCH_CODE) as string[]; - registration.ProviderKey = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_PROVIDER_KEY) as string; - registration.Tag = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_TAG) as string; - registration.UpgradeCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE) as string[]; - registration.Version = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION) as string; - registration.DisplayName = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_DISPLAY_NAME) as string; - registration.EngineVersion = idKey.GetValue(BURN_REGISTRATION_REGISTRY_ENGINE_VERSION) as string; - registration.EstimatedSize = idKey.GetValue(REGISTRY_BUNDLE_ESTIMATED_SIZE) as int?; - registration.Installed = idKey.GetValue(REGISTRY_BUNDLE_INSTALLED) as int?; - registration.ModifyPath = idKey.GetValue(REGISTRY_BUNDLE_MODIFY_PATH) as string; - registration.Publisher = idKey.GetValue(REGISTRY_BUNDLE_PUBLISHER) as string; - registration.UrlInfoAbout = idKey.GetValue(REGISTRY_BUNDLE_URL_INFO_ABOUT) as string; - registration.UrlUpdateInfo = idKey.GetValue(REGISTRY_BUNDLE_URL_UPDATE_INFO) as string; - - registration.QuietUninstallString = idKey.GetValue(REGISTRY_BUNDLE_QUIET_UNINSTALL_STRING) as string; - if (!String.IsNullOrEmpty(registration.QuietUninstallString)) - { - var closeQuote = registration.QuietUninstallString.IndexOf("\"", 1); - if (closeQuote > 0) - { - registration.QuietUninstallCommand = registration.QuietUninstallString.Substring(1, closeQuote - 1).Trim(); - registration.QuietUninstallCommandArguments = registration.QuietUninstallString.Substring(closeQuote + 1).Trim(); - } - } - - registration.UninstallString = idKey.GetValue(REGISTRY_BUNDLE_UNINSTALL_STRING) as string; - if (!String.IsNullOrEmpty(registration.UninstallString)) - { - var closeQuote = registration.UninstallString.IndexOf("\"", 1); - if (closeQuote > 0) - { - registration.UninstallCommand = registration.UninstallString.Substring(1, closeQuote - 1).Trim(); - registration.UninstallCommandArguments = registration.UninstallString.Substring(closeQuote + 1).Trim(); - } - } - - return registration; - } - } -} diff --git a/src/WixToolsetTest.BurnE2E/BundleVerifier.cs b/src/WixToolsetTest.BurnE2E/BundleVerifier.cs deleted file mode 100644 index 98ec96a0..00000000 --- a/src/WixToolsetTest.BurnE2E/BundleVerifier.cs +++ /dev/null @@ -1,96 +0,0 @@ -// 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.IO; - using System.Linq; - using System.Text; - using Microsoft.Win32; - using WixToolset.Data; - using WixToolset.Data.Symbols; - using Xunit; - - public partial class BundleInstaller - { - public const string FULL_BURN_POLICY_REGISTRY_PATH = "SOFTWARE\\WOW6432Node\\Policies\\WiX\\Burn"; - public const string PACKAGE_CACHE_FOLDER_NAME = "Package Cache"; - - public string BundlePdb { get; } - - private WixBundleSymbol BundleSymbol { get; set; } - - private WixBundleSymbol GetBundleSymbol() - { - if (this.BundleSymbol == null) - { - using var wixOutput = WixOutput.Read(this.BundlePdb); - var intermediate = Intermediate.Load(wixOutput); - var section = intermediate.Sections.Single(); - this.BundleSymbol = section.Symbols.OfType().Single(); - } - - return this.BundleSymbol; - } - - public string GetPackageCachePathForCacheId(string cacheId) - { - using var policyKey = Registry.LocalMachine.OpenSubKey(FULL_BURN_POLICY_REGISTRY_PATH); - var redirectedCachePath = policyKey?.GetValue("PackageCache") as string; - var cachePath = redirectedCachePath ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), PACKAGE_CACHE_FOLDER_NAME); - return Path.Combine(cachePath, cacheId); - } - - public string GetExpectedCachedBundlePath() - { - var bundleSymbol = this.GetBundleSymbol(); - var cachePath = this.GetPackageCachePathForCacheId(bundleSymbol.BundleId); - return Path.Combine(cachePath, Path.GetFileName(this.Bundle)); - } - - public bool TryGetPerMachineRegistration(out BundleRegistration registration) - { - var bundleSymbol = this.GetBundleSymbol(); - var bundleId = bundleSymbol.BundleId; - return BundleRegistration.TryGetPerMachineBundleRegistrationById(bundleId, out registration); - } - - public string VerifyRegisteredAndInPackageCache() - { - Assert.True(this.TryGetPerMachineRegistration(out var registration)); - - Assert.NotNull(registration.CachePath); - Assert.True(File.Exists(registration.CachePath)); - - var expectedCachePath = this.GetExpectedCachedBundlePath(); - Assert.Equal(expectedCachePath, registration.CachePath, StringComparer.OrdinalIgnoreCase); - - return registration.CachePath; - } - - public void VerifyUnregisteredAndRemovedFromPackageCache() - { - var cachedBundlePath = this.GetExpectedCachedBundlePath(); - this.VerifyUnregisteredAndRemovedFromPackageCache(cachedBundlePath); - } - - public void VerifyUnregisteredAndRemovedFromPackageCache(string cachedBundlePath) - { - Assert.False(this.TryGetPerMachineRegistration(out _)); - Assert.False(File.Exists(cachedBundlePath)); - } - - public void RemovePackageFromCache(string packageId) - { - using var wixOutput = WixOutput.Read(this.BundlePdb); - var intermediate = Intermediate.Load(wixOutput); - var section = intermediate.Sections.Single(); - var packageSymbol = section.Symbols.OfType().Single(p => p.Id.Id == packageId); - var cachePath = this.GetPackageCachePathForCacheId(packageSymbol.CacheId); - if (Directory.Exists(cachePath)) - { - Directory.Delete(cachePath, true); - } - } - } -} diff --git a/src/WixToolsetTest.BurnE2E/BurnE2ETests.cs b/src/WixToolsetTest.BurnE2E/BurnE2ETests.cs index d3400658..c9294f73 100644 --- a/src/WixToolsetTest.BurnE2E/BurnE2ETests.cs +++ b/src/WixToolsetTest.BurnE2E/BurnE2ETests.cs @@ -4,6 +4,7 @@ namespace WixToolsetTest.BurnE2E { using System; using System.Collections.Generic; + using WixTestTools; using Xunit; using Xunit.Abstractions; diff --git a/src/WixToolsetTest.BurnE2E/FailureTests.cs b/src/WixToolsetTest.BurnE2E/FailureTests.cs index ba6e5ba4..bc505527 100644 --- a/src/WixToolsetTest.BurnE2E/FailureTests.cs +++ b/src/WixToolsetTest.BurnE2E/FailureTests.cs @@ -2,6 +2,7 @@ namespace WixToolsetTest.BurnE2E { + using WixTestTools; using Xunit; using Xunit.Abstractions; diff --git a/src/WixToolsetTest.BurnE2E/MSIExec.cs b/src/WixToolsetTest.BurnE2E/MSIExec.cs deleted file mode 100644 index 659d91ea..00000000 --- a/src/WixToolsetTest.BurnE2E/MSIExec.cs +++ /dev/null @@ -1,753 +0,0 @@ -// 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.IO; - using System.Text; - using WixBuildTools.TestSupport; - - public class MSIExec : TestTool - { - /// - /// The expected exit code of the tool - /// - public new MSIExecReturnCode ExpectedExitCode - { - get { return (MSIExecReturnCode)base.ExpectedExitCode; } - set { base.ExpectedExitCode = (int?)value; } - } - - /// - /// Mode of execution (install, uninstall, or repair) - /// - public MSIExecMode ExecutionMode { get; set; } - - /// - /// Path to msi or ProductCode - /// - public string Product { get; set; } - - /// - /// Logging Options - /// - public MSIExecLoggingOptions LoggingOptions { get; set; } - - /// - /// Path to the log file - /// - public string LogFile { get; set; } - - /// - /// Unattended mode - progress bar only - /// - public bool Passive { get; set; } - - /// - /// Quiet mode, no user interaction - /// - public bool Quiet { get; set; } - - /// - /// Sets user interface level - /// - public MSIExecUserInterfaceLevel UserInterfaceLevel { get; set; } - - /// - /// Do not restart after the installation is complete - /// - public bool NoRestart { get; set; } - - /// - /// Prompts the user for restart if necessary - /// - public bool PromptRestart { get; set; } - - /// - /// Always restart the computer after installation - /// - public bool ForceRestart { get; set; } - - /// - /// Other arguments. - /// - public string OtherArguments { get; set; } - - /// - /// Constructor that uses the default location for MSIExec. - /// - public MSIExec() - : this(Environment.SystemDirectory) - { - } - - /// - /// Constructor that accepts a path to the MSIExec location. - /// - /// The directory of MSIExec.exe. - public MSIExec(string toolDirectory) - : base(Path.Combine(toolDirectory, "MSIExec.exe")) - { - this.SetDefaultArguments(); - } - - public override ExternalExecutableResult Run(bool assertOnError) - { - this.Arguments = this.GetArguments(); - return base.Run(assertOnError); - } - - /// - /// Clears all of the assigned arguments and resets them to the default values. - /// - public void SetDefaultArguments() - { - this.ExecutionMode = MSIExecMode.Install; - this.Product = String.Empty; - this.Quiet = true; - this.Passive = false; - this.UserInterfaceLevel = MSIExecUserInterfaceLevel.None; - this.NoRestart = true; - this.ForceRestart = false; - this.PromptRestart = false; - this.LogFile = string.Empty; - this.LoggingOptions = MSIExecLoggingOptions.VOICEWARMUP; - this.OtherArguments = String.Empty; - } - - public string GetArguments() - { - var arguments = new StringBuilder(); - - // quiet - if (this.Quiet) - { - arguments.Append(" /quiet "); - } - - // passive - if (this.Passive) - { - arguments.Append(" /passive "); - } - - // UserInterfaceLevel - switch (this.UserInterfaceLevel) - { - case MSIExecUserInterfaceLevel.None: - arguments.Append(" /qn "); - break; - case MSIExecUserInterfaceLevel.Basic: - arguments.Append(" /qb "); - break; - case MSIExecUserInterfaceLevel.Reduced: - arguments.Append(" /qr "); - break; - case MSIExecUserInterfaceLevel.Full: - arguments.Append(" /qf "); - break; - } - - // NoRestart - if (this.NoRestart) - { - arguments.Append(" /norestart "); - } - - // PromptRestart - if (this.PromptRestart) - { - arguments.Append(" /promptrestart "); - } - - // ForceRestart - if (this.ForceRestart) - { - arguments.Append(" /forcerestart "); - } - - // Logging options - var loggingOptionsString = new StringBuilder(); - if ((this.LoggingOptions & MSIExecLoggingOptions.Status_Messages) == MSIExecLoggingOptions.Status_Messages) - { - loggingOptionsString.Append("i"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.Nonfatal_Warnings) == MSIExecLoggingOptions.Nonfatal_Warnings) - { - loggingOptionsString.Append("w"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.All_Error_Messages) == MSIExecLoggingOptions.All_Error_Messages) - { - loggingOptionsString.Append("e"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.Start_Up_Of_Actions) == MSIExecLoggingOptions.Start_Up_Of_Actions) - { - loggingOptionsString.Append("a"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.Action_Specific_Records) == MSIExecLoggingOptions.Action_Specific_Records) - { - loggingOptionsString.Append("r"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.User_Requests) == MSIExecLoggingOptions.User_Requests) - { - loggingOptionsString.Append("u"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.Initial_UI_Parameters) == MSIExecLoggingOptions.Initial_UI_Parameters) - { - loggingOptionsString.Append("c"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.OutOfMemory_Or_Fatal_Exit_Information) == MSIExecLoggingOptions.OutOfMemory_Or_Fatal_Exit_Information) - { - loggingOptionsString.Append("m"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.OutOfDiskSpace_Messages) == MSIExecLoggingOptions.OutOfDiskSpace_Messages) - { - loggingOptionsString.Append("o"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.Terminal_Properties) == MSIExecLoggingOptions.Terminal_Properties) - { - loggingOptionsString.Append("p"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.Verbose_Output) == MSIExecLoggingOptions.Verbose_Output) - { - loggingOptionsString.Append("v"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.Extra_Debugging_Information) == MSIExecLoggingOptions.Extra_Debugging_Information) - { - loggingOptionsString.Append("x"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.Append_To_Existing_Log_File) == MSIExecLoggingOptions.Append_To_Existing_Log_File) - { - loggingOptionsString.Append("+"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.Flush_Each_line) == MSIExecLoggingOptions.Flush_Each_line) - { - loggingOptionsString.Append("!"); - } - if ((this.LoggingOptions & MSIExecLoggingOptions.Log_All_Information) == MSIExecLoggingOptions.Log_All_Information) - { - loggingOptionsString.Append("*"); - } - - // logfile and logging options - if (0 != loggingOptionsString.Length || !string.IsNullOrEmpty(this.LogFile)) - { - arguments.Append(" /l"); - if (0 != loggingOptionsString.Length) - { - arguments.AppendFormat("{0} ", loggingOptionsString); - } - if (!string.IsNullOrEmpty(this.LogFile)) - { - arguments.AppendFormat(" \"{0}\" ", this.LogFile); - } - } - - // OtherArguments - if (!String.IsNullOrEmpty(this.OtherArguments)) - { - arguments.AppendFormat(" {0} ", this.OtherArguments); - } - - // execution mode - switch (this.ExecutionMode) - { - case MSIExecMode.Install: - arguments.Append(" /package "); - break; - case MSIExecMode.AdministrativeInstall: - arguments.Append(" /a "); - break; - case MSIExecMode.Repair: - arguments.Append(" /f "); - break; - case MSIExecMode.Cleanup: - case MSIExecMode.Uninstall: - arguments.Append(" /uninstall "); - break; - }; - - // product - if (!string.IsNullOrEmpty(this.Product)) - { - arguments.AppendFormat(" \"{0}\" ", this.Product); - } - - return arguments.ToString(); - } - - /// - /// Return codes from an MSI install or uninstall - /// - /// - /// Error codes indicative of success are: - /// ERROR_SUCCESS, ERROR_SUCCESS_REBOOT_INITIATED, and ERROR_SUCCESS_REBOOT_REQUIRED - /// - public enum MSIExecReturnCode - { - /// - /// ERROR_SUCCESS 0 - /// Action completed successfully. - /// - SUCCESS = 0, - - /// - /// ERROR_INVALID_DATA 13 - /// The data is invalid. - /// - ERROR_INVALID_DATA = 13, - - /// - /// ERROR_INVALID_PARAMETER 87 - /// One of the parameters was invalid. - /// - ERROR_INVALID_PARAMETER = 87, - - /// - /// ERROR_CALL_NOT_IMPLEMENTED 120 - /// This value is returned when a custom action attempts to call a function that cannot be called from custom actions. - /// The function returns the value ERROR_CALL_NOT_IMPLEMENTED. Available beginning with Windows Installer version 3.0. - /// - ERROR_CALL_NOT_IMPLEMENTED = 120, - - /// - /// ERROR_APPHELP_BLOCK 1259 - /// If Windows Installer determines a product may be incompatible with the current operating system, - /// it displays a dialog box informing the user and asking whether to try to install anyway. - /// This error code is returned if the user chooses not to try the installation. - /// - ERROR_APPHELP_BLOCK = 1259, - - /// - /// ERROR_INSTALL_SERVICE_FAILURE 1601 - /// The Windows Installer service could not be accessed. - /// Contact your support personnel to verify that the Windows Installer service is properly registered. - /// - ERROR_INSTALL_SERVICE_FAILURE = 1601, - - - /// - /// ERROR_INSTALL_USEREXIT 1602 - /// The user cancels installation. - /// - ERROR_INSTALL_USEREXIT = 1602, - - /// - /// ERROR_INSTALL_FAILURE 1603 - /// A fatal error occurred during installation. - /// - ERROR_INSTALL_FAILURE = 1603, - - /// - /// ERROR_INSTALL_SUSPEND 1604 - /// Installation suspended, incomplete. - /// - ERROR_INSTALL_SUSPEND = 1604, - - /// - /// ERROR_UNKNOWN_PRODUCT 1605 - /// This action is only valid for products that are currently installed. - /// - ERROR_UNKNOWN_PRODUCT = 1605, - - /// - /// ERROR_UNKNOWN_FEATURE 1606 - /// The feature identifier is not registered. - /// - ERROR_UNKNOWN_FEATURE = 1606, - - /// - /// ERROR_UNKNOWN_COMPONENT 1607 - /// The component identifier is not registered. - /// - ERROR_UNKNOWN_COMPONENT = 1607, - - /// - /// ERROR_UNKNOWN_PROPERTY 1608 - /// This is an unknown property. - /// - ERROR_UNKNOWN_PROPERTY = 1608, - - /// - /// ERROR_INVALID_HANDLE_STATE 1609 - /// The handle is in an invalid state. - /// - ERROR_INVALID_HANDLE_STATE = 1609, - - /// - /// ERROR_BAD_CONFIGURATION 1610 - /// The configuration data for this product is corrupt. Contact your support personnel. - /// - ERROR_BAD_CONFIGURATION = 1610, - - /// - /// ERROR_INDEX_ABSENT 1611 - /// The component qualifier not present. - /// - ERROR_INDEX_ABSENT = 1611, - - /// ERROR_INSTALL_SOURCE_ABSENT 1612 - /// The installation source for this product is not available. - /// Verify that the source exists and that you can access it. - /// - ERROR_INSTALL_SOURCE_ABSENT = 1612, - - /// - /// ERROR_INSTALL_PACKAGE_VERSION 1613 - /// This installation package cannot be installed by the Windows Installer service. - /// You must install a Windows service pack that contains a newer version of the Windows Installer service. - /// - ERROR_INSTALL_PACKAGE_VERSION = 1613, - - /// - /// ERROR_PRODUCT_UNINSTALLED 1614 - /// The product is uninstalled. - /// - ERROR_PRODUCT_UNINSTALLED = 1614, - - /// - /// ERROR_BAD_QUERY_SYNTAX 1615 - /// The SQL query syntax is invalid or unsupported. - /// - ERROR_BAD_QUERY_SYNTAX = 1615, - - /// - /// ERROR_INVALID_FIELD 1616 - /// The record field does not exist. - /// - ERROR_INVALID_FIELD = 1616, - - /// - /// ERROR_INSTALL_ALREADY_RUNNING 1618 - /// Another installation is already in progress. Complete that installation before proceeding with this install. - /// For information about the mutex, see _MSIExecute Mutex. - /// - ERROR_INSTALL_ALREADY_RUNNING = 1618, - - /// - /// ERROR_INSTALL_PACKAGE_OPEN_FAILED 1619 - /// This installation package could not be opened. Verify that the package exists and is accessible, or contact the - /// application vendor to verify that this is a valid Windows Installer package. - /// - ERROR_INSTALL_PACKAGE_OPEN_FAILED = 1619, - - - /// - /// ERROR_INSTALL_PACKAGE_INVALID 1620 - /// This installation package could not be opened. - /// Contact the application vendor to verify that this is a valid Windows Installer package. - /// - ERROR_INSTALL_PACKAGE_INVALID = 1620, - - /// - /// ERROR_INSTALL_UI_FAILURE 1621 - /// There was an error starting the Windows Installer service user interface. - /// Contact your support personnel. - /// - ERROR_INSTALL_UI_FAILURE = 1621, - - /// - /// ERROR_INSTALL_LOG_FAILURE 1622 - /// There was an error opening installation log file. - /// Verify that the specified log file location exists and is writable. - /// - ERROR_INSTALL_LOG_FAILURE = 1622, - - /// - /// ERROR_INSTALL_LANGUAGE_UNSUPPORTED 1623 - /// This language of this installation package is not supported by your system. - /// - ERROR_INSTALL_LANGUAGE_UNSUPPORTED = 1623, - - /// - /// ERROR_INSTALL_TRANSFORM_FAILURE 1624 - /// There was an error applying transforms. - /// Verify that the specified transform paths are valid. - /// - ERROR_INSTALL_TRANSFORM_FAILURE = 1624, - - - /// - /// ERROR_INSTALL_PACKAGE_REJECTED 1625 - /// This installation is forbidden by system policy. - /// Contact your system administrator. - /// - ERROR_INSTALL_PACKAGE_REJECTED = 1625, - - /// - /// ERROR_FUNCTION_NOT_CALLED 1626 - /// The function could not be executed. - /// - ERROR_FUNCTION_NOT_CALLED = 1626, - - /// - /// ERROR_FUNCTION_FAILED 1627 - /// The function failed during execution. - /// - ERROR_FUNCTION_FAILED = 1627, - - /// - /// ERROR_INVALID_TABLE 1628 - /// An invalid or unknown table was specified. - /// - ERROR_INVALID_TABLE = 1628, - - /// - /// ERROR_DATATYPE_MISMATCH 1629 - /// The data supplied is the wrong type. - /// - ERROR_DATATYPE_MISMATCH = 1629, - - /// - /// ERROR_UNSUPPORTED_TYPE 1630 - /// Data of this type is not supported. - /// - ERROR_UNSUPPORTED_TYPE = 1630, - - /// - /// ERROR_CREATE_FAILED 1631 - /// The Windows Installer service failed to start. - /// Contact your support personnel. - /// - ERROR_CREATE_FAILED = 1631, - - /// - /// ERROR_INSTALL_TEMP_UNWRITABLE 1632 - /// The Temp folder is either full or inaccessible. - /// Verify that the Temp folder exists and that you can write to it. - /// - ERROR_INSTALL_TEMP_UNWRITABLE = 1632, - - /// - /// ERROR_INSTALL_PLATFORM_UNSUPPORTED 1633 - /// This installation package is not supported on this platform. Contact your application vendor. - ERROR_INSTALL_PLATFORM_UNSUPPORTED = 1633, - - /// - /// ERROR_INSTALL_NOTUSED 1634 - /// Component is not used on this machine. - /// - ERROR_INSTALL_NOTUSED = 1634, - - /// - /// ERROR_PATCH_PACKAGE_OPEN_FAILED 1635 - /// This patch package could not be opened. Verify that the patch package exists and is accessible, - /// or contact the application vendor to verify that this is a valid Windows Installer patch package. - /// - ERROR_PATCH_PACKAGE_OPEN_FAILED = 1635, - - /// - /// ERROR_PATCH_PACKAGE_INVALID 1636 - /// This patch package could not be opened. - /// Contact the application vendor to verify that this is a valid Windows Installer patch package. - /// - ERROR_PATCH_PACKAGE_INVALID = 1636, - - /// - /// ERROR_PATCH_PACKAGE_UNSUPPORTED 1637 - /// This patch package cannot be processed by the Windows Installer service. - /// You must install a Windows service pack that contains a newer version of the Windows Installer service. - /// - ERROR_PATCH_PACKAGE_UNSUPPORTED = 1637, - - /// - /// ERROR_PRODUCT_VERSION 1638 - /// Another version of this product is already installed. - /// Installation of this version cannot continue. To configure or remove the existing version of this product, - /// use Add/Remove Programs in Control Panel. - /// - ERROR_PRODUCT_VERSION = 1638, - - /// - /// ERROR_INVALID_COMMAND_LINE 1639 - /// Invalid command line argument. - /// Consult the Windows Installer SDK for detailed command-line help. - /// - ERROR_INVALID_COMMAND_LINE = 1639, - - /// - /// ERROR_INSTALL_REMOTE_DISALLOWED 1640 - /// The current user is not permitted to perform installations from a client session of a server running the - /// Terminal Server role service. - /// - ERROR_INSTALL_REMOTE_DISALLOWED = 1640, - - /// - /// ERROR_SUCCESS_REBOOT_INITIATED 1641 - /// The installer has initiated a restart. - /// This message is indicative of a success. - /// - ERROR_SUCCESS_REBOOT_INITIATED = 1641, - - /// - /// ERROR_PATCH_TARGET_NOT_FOUND 1642 - /// The installer cannot install the upgrade patch because the program being upgraded may be missing or the - /// upgrade patch updates a different version of the program. - /// Verify that the program to be upgraded exists on your computer and that you have the correct upgrade patch. - /// - ERROR_PATCH_TARGET_NOT_FOUND = 1642, - - /// - /// ERROR_PATCH_PACKAGE_REJECTED 1643 - /// The patch package is not permitted by system policy. - /// - ERROR_PATCH_PACKAGE_REJECTED = 1643, - - /// - /// ERROR_INSTALL_TRANSFORM_REJECTED 1644 - /// One or more customizations are not permitted by system policy. - /// - ERROR_INSTALL_TRANSFORM_REJECTED = 1644, - - /// - /// ERROR_INSTALL_REMOTE_PROHIBITED 1645 - /// Windows Installer does not permit installation from a Remote Desktop Connection. - /// - ERROR_INSTALL_REMOTE_PROHIBITED = 1645, - - /// - /// ERROR_PATCH_REMOVAL_UNSUPPORTED 1646 - /// The patch package is not a removable patch package. Available beginning with Windows Installer version 3.0. - /// - ERROR_PATCH_REMOVAL_UNSUPPORTED = 1646, - - /// - /// ERROR_UNKNOWN_PATCH 1647 - /// The patch is not applied to this product. Available beginning with Windows Installer version 3.0. - /// - ERROR_UNKNOWN_PATCH = 1647, - - /// - /// ERROR_PATCH_NO_SEQUENCE 1648 - /// No valid sequence could be found for the set of patches. Available beginning with Windows Installer version 3.0. - /// - ERROR_PATCH_NO_SEQUENCE = 1648, - - /// - /// ERROR_PATCH_REMOVAL_DISALLOWED 1649 - /// Patch removal was disallowed by policy. Available beginning with Windows Installer version 3.0. - ERROR_PATCH_REMOVAL_DISALLOWED = 1649, - - /// - /// ERROR_INVALID_PATCH_XML = 1650 - /// The XML patch data is invalid. Available beginning with Windows Installer version 3.0. - /// - ERROR_INVALID_PATCH_XML = 1650, - - /// - /// ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT 1651 - /// Administrative user failed to apply patch for a per-user managed or a per-machine application that is in advertise state. - /// Available beginning with Windows Installer version 3.0. - ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT = 1651, - - /// - /// ERROR_INSTALL_SERVICE_SAFEBOOT 1652 - /// Windows Installer is not accessible when the computer is in Safe Mode. - /// Exit Safe Mode and try again or try using System Restore to return your computer to a previous state. - /// Available beginning with Windows Installer version 4.0. - /// - ERROR_INSTALL_SERVICE_SAFEBOOT = 1652, - - /// - /// ERROR_ROLLBACK_DISABLED 1653 - /// Could not perform a multiple-package transaction because rollback has been disabled. - /// Multiple-Package Installations cannot run if rollback is disabled. Available beginning with Windows Installer version 4.5. - /// - ERROR_ROLLBACK_DISABLED = 1653, - - /// - /// ERROR_SUCCESS_REBOOT_REQUIRED 3010 - /// A restart is required to complete the install. This message is indicative of a success. - /// This does not include installs where the ForceReboot action is run. - /// - ERROR_SUCCESS_REBOOT_REQUIRED = 3010 - } - - /// - /// Modes of operations for MSIExec; install, administrator install, uninstall .. etc - /// - public enum MSIExecMode - { - /// - /// Installs or configures a product - /// - Install = 0, - - /// - /// Administrative install - Installs a product on the network - /// - AdministrativeInstall, - - /// - /// Uninstalls the product - /// - Uninstall, - - /// - /// Repairs a product - /// - Repair, - - /// - /// Modifies a product - /// - Modify, - - /// - /// Uninstalls the product as part of cleanup - /// - Cleanup, - } - - /// - /// User interfave levels - /// - public enum MSIExecUserInterfaceLevel - { - /// - /// No UI - /// - None = 0, - - /// - /// Basic UI - /// - Basic, - - /// - /// Reduced UI - /// - Reduced, - - /// - /// Full UI (default) - /// - Full - } - - /// - /// Logging options - /// - [Flags] - public enum MSIExecLoggingOptions - { - Status_Messages = 0x0001, - Nonfatal_Warnings = 0x0002, - All_Error_Messages = 0x0004, - Start_Up_Of_Actions = 0x0008, - Action_Specific_Records = 0x0010, - User_Requests = 0x0020, - Initial_UI_Parameters = 0x0040, - OutOfMemory_Or_Fatal_Exit_Information = 0x0080, - OutOfDiskSpace_Messages = 0x0100, - Terminal_Properties = 0x0200, - Verbose_Output = 0x0400, - Append_To_Existing_Log_File = 0x0800, - - Flush_Each_line = 0x1000, - Extra_Debugging_Information = 0x2000, - Log_All_Information = 0x4000, - VOICEWARMUP = 0x0FFF - } - } -} diff --git a/src/WixToolsetTest.BurnE2E/MsiTransactionTests.cs b/src/WixToolsetTest.BurnE2E/MsiTransactionTests.cs index 6d8f1536..3d9748bb 100644 --- a/src/WixToolsetTest.BurnE2E/MsiTransactionTests.cs +++ b/src/WixToolsetTest.BurnE2E/MsiTransactionTests.cs @@ -4,6 +4,7 @@ namespace WixToolsetTest.BurnE2E { using System; using System.IO; + using WixTestTools; using Xunit; using Xunit.Abstractions; diff --git a/src/WixToolsetTest.BurnE2E/MsiUtilities.cs b/src/WixToolsetTest.BurnE2E/MsiUtilities.cs deleted file mode 100644 index 1a9f0925..00000000 --- a/src/WixToolsetTest.BurnE2E/MsiUtilities.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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 WixToolset.Dtf.WindowsInstaller; - - public class MsiUtilities - { - /// - /// Return true if it finds the given productcode in system otherwise it returns false - /// - /// - /// - public static bool IsProductInstalled(string prodCode) - { - //look in all user's products (both per-machine and per-user) - foreach (ProductInstallation product in ProductInstallation.GetProducts(null, "s-1-1-0", UserContexts.All)) - { - if (product.ProductCode == prodCode) - { - return true; - } - } - return false; - } - } -} diff --git a/src/WixToolsetTest.BurnE2E/PackageInstaller.cs b/src/WixToolsetTest.BurnE2E/PackageInstaller.cs deleted file mode 100644 index c3516fe7..00000000 --- a/src/WixToolsetTest.BurnE2E/PackageInstaller.cs +++ /dev/null @@ -1,90 +0,0 @@ -// 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.IO; - using static WixToolsetTest.BurnE2E.MSIExec; - - public partial class PackageInstaller : IDisposable - { - public PackageInstaller(WixTestContext testContext, string filename) - { - this.Package = Path.Combine(testContext.TestDataFolder, $"{filename}.msi"); - this.PackagePdb = Path.Combine(testContext.TestDataFolder, $"{filename}.wixpdb"); - this.TestContext = testContext; - } - - public string Package { get; } - - private WixTestContext TestContext { get; } - - public string TestGroupName => this.TestContext.TestGroupName; - - public string TestName => this.TestContext.TestName; - - /// - /// Installs a .msi file - /// - /// Expected exit code - /// Other arguments to pass to MSIExec. - /// MSIExec log File - public string InstallProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments) - { - return this.RunMSIExec(MSIExecMode.Install, otherArguments, expectedExitCode); - } - - /// - /// Uninstalls a .msi file - /// - /// Expected exit code - /// Other arguments to pass to MSIExec. - /// MSIExec log File - public string UninstallProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments) - { - return this.RunMSIExec(MSIExecMode.Uninstall, otherArguments, expectedExitCode); - } - - /// - /// Repairs a .msi file - /// - /// Expected exit code - /// Other arguments to pass to msiexe.exe. - /// MSIExec log File - public string RepairProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments) - { - return this.RunMSIExec(MSIExecMode.Repair, otherArguments, expectedExitCode); - } - - /// - /// Executes MSIExec on a .msi file - /// - /// Mode of execution for MSIExec - /// Other arguments to pass to MSIExec. - /// Expected exit code - /// MSIExec exit code - private string RunMSIExec(MSIExecMode mode, string[] otherArguments, MSIExecReturnCode expectedExitCode, bool assertOnError = true) - { - // Generate the log file name. - var logFile = Path.Combine(Path.GetTempPath(), String.Format("{0}_{1}_{2:yyyyMMddhhmmss}_{4}_{3}.log", this.TestGroupName, this.TestName, DateTime.UtcNow, Path.GetFileNameWithoutExtension(this.Package), mode)); - - var msiexec = new MSIExec - { - Product = this.Package, - ExecutionMode = mode, - OtherArguments = null != otherArguments ? String.Join(" ", otherArguments) : null, - ExpectedExitCode = expectedExitCode, - LogFile = logFile, - }; - - msiexec.Run(assertOnError); - return msiexec.LogFile; - } - - public void Dispose() - { - string[] args = { "IGNOREDEPENDENCIES=ALL", "WIXFAILWHENDEFERRED=0" }; - this.RunMSIExec(MSIExecMode.Cleanup, args, MSIExecReturnCode.SUCCESS, assertOnError: false); - } - } -} diff --git a/src/WixToolsetTest.BurnE2E/PackageVerifier.cs b/src/WixToolsetTest.BurnE2E/PackageVerifier.cs deleted file mode 100644 index 7a4fea88..00000000 --- a/src/WixToolsetTest.BurnE2E/PackageVerifier.cs +++ /dev/null @@ -1,77 +0,0 @@ -// 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.IO; - using System.Linq; - using WixToolset.Data; - using WixToolset.Data.WindowsInstaller; - using WixToolset.Data.WindowsInstaller.Rows; - using Xunit; - - public partial class PackageInstaller - { - public string PackagePdb { get; } - - private WindowsInstallerData WiData { get; set; } - - public string GetInstalledFilePath(string filename) - { - return this.TestContext.GetTestInstallFolder(Path.Combine(this.GetInstallFolderName(), filename)); - } - - private WindowsInstallerData GetWindowsInstallerData() - { - if (this.WiData == null) - { - using var wixOutput = WixOutput.Read(this.PackagePdb); - this.WiData = WindowsInstallerData.Load(wixOutput); - } - - return this.WiData; - } - - public string GetInstallFolderName() - { - var wiData = this.GetWindowsInstallerData(); - var row = wiData.Tables["Directory"].Rows.Single(r => r.FieldAsString(0) == "INSTALLFOLDER"); - var value = row.FieldAsString(2); - var longNameIndex = value.IndexOf('|') + 1; - if (longNameIndex > 0) - { - return value.Substring(longNameIndex); - } - return value; - } - - public string GetProperty(string name) - { - var wiData = this.GetWindowsInstallerData(); - var row = wiData.Tables["Property"].Rows.Cast().Single(r => r.Property == name); - return row.Value; - } - - public void VerifyInstalled(bool installed) - { - var productCode = this.GetProperty("ProductCode"); - Assert.Equal(installed, MsiUtilities.IsProductInstalled(productCode)); - } - - public void VerifyTestRegistryRootDeleted() - { - using var testRegistryRoot = this.TestContext.GetTestRegistryRoot(); - Assert.Null(testRegistryRoot); - } - - public void VerifyTestRegistryValue(string name, string expectedValue) - { - using (var root = this.TestContext.GetTestRegistryRoot()) - { - Assert.NotNull(root); - var actualValue = root.GetValue(name) as string; - Assert.Equal(expectedValue, actualValue); - } - } - } -} diff --git a/src/WixToolsetTest.BurnE2E/TestBAController.cs b/src/WixToolsetTest.BurnE2E/TestBAController.cs index 91d1b817..1b254656 100644 --- a/src/WixToolsetTest.BurnE2E/TestBAController.cs +++ b/src/WixToolsetTest.BurnE2E/TestBAController.cs @@ -4,6 +4,7 @@ namespace WixToolsetTest.BurnE2E { using System; using Microsoft.Win32; + using WixTestTools; using WixToolset.Mba.Core; public class TestBAController : IDisposable diff --git a/src/WixToolsetTest.BurnE2E/TestTool.cs b/src/WixToolsetTest.BurnE2E/TestTool.cs deleted file mode 100644 index e35c5c4b..00000000 --- a/src/WixToolsetTest.BurnE2E/TestTool.cs +++ /dev/null @@ -1,245 +0,0 @@ -// 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.Collections.Generic; - using System.Text; - using System.Text.RegularExpressions; - using WixBuildTools.TestSupport; - using Xunit; - - public class TestTool : ExternalExecutable - { - /// - /// Constructor for a TestTool - /// - public TestTool() - : this(null) - { - } - - /// - /// Constructor for a TestTool - /// - /// The full path to the tool. Eg. c:\bin\candle.exe - public TestTool(string toolFile) - : base(toolFile) - { - this.PrintOutputToConsole = true; - } - - /// - /// The arguments to pass to the tool - /// - public virtual string Arguments { get; set; } - - /// - /// Stores the errors that occurred when a run was checked against its expected results - /// - public List Errors { get; set; } - - /// - /// A list of Regex's that are expected to match stderr - /// - public List ExpectedErrorRegexs { get; set; } = new List(); - - /// - /// The expected error strings to stderr - /// - public List ExpectedErrorStrings { get; set; } = new List(); - - /// - /// The expected exit code of the tool - /// - public int? ExpectedExitCode { get; set; } - - /// - /// A list of Regex's that are expected to match stdout - /// - public List ExpectedOutputRegexs { get; set; } = new List(); - - /// - /// The expected output strings to stdout - /// - public List ExpectedOutputStrings { get; set; } = new List(); - - /// - /// Print output from the tool execution to the console - /// - public bool PrintOutputToConsole { get; set; } - - /// - /// The working directory of the tool - /// - public string WorkingDirectory { get; set; } - - /// - /// Print the errors from the last run - /// - public void PrintErrors() - { - if (null != this.Errors) - { - Console.WriteLine("Errors:"); - - foreach (string error in this.Errors) - { - Console.WriteLine(error); - } - } - } - - /// - /// Run the tool - /// - /// The results of the run - public ExternalExecutableResult Run() - { - return this.Run(true); - } - - /// - /// Run the tool - /// - /// Throw an exception if the expected results don't match the actual results - /// Thrown when the expected results don't match the actual results - /// The results of the run - public virtual ExternalExecutableResult Run(bool assertOnError) - { - var result = this.Run(this.Arguments, workingDirectory: this.WorkingDirectory ?? String.Empty); - - if (this.PrintOutputToConsole) - { - Console.WriteLine(FormatResult(result)); - } - - this.Errors = this.CheckResult(result); - - if (assertOnError && 0 < this.Errors.Count) - { - if (this.PrintOutputToConsole) - { - this.PrintErrors(); - } - - Assert.Empty(this.Errors); - } - - return result; - } - - /// - /// Checks that the result from a run matches the expected results - /// - /// A result from a run - /// A list of errors - public virtual List CheckResult(ExternalExecutableResult result) - { - List errors = new List(); - - // Verify that the expected return code matched the actual return code - if (null != this.ExpectedExitCode && this.ExpectedExitCode != result.ExitCode) - { - errors.Add(String.Format("Expected exit code {0} did not match actual exit code {1}", this.ExpectedExitCode, result.ExitCode)); - } - - var standardErrorString = string.Join(Environment.NewLine, result.StandardError); - - // Verify that the expected error string are in stderr - if (null != this.ExpectedErrorStrings) - { - foreach (string expectedString in this.ExpectedErrorStrings) - { - if (!standardErrorString.Contains(expectedString)) - { - errors.Add(String.Format("The text '{0}' was not found in stderr", expectedString)); - } - } - } - - var standardOutputString = string.Join(Environment.NewLine, result.StandardOutput); - - // Verify that the expected output string are in stdout - if (null != this.ExpectedOutputStrings) - { - foreach (string expectedString in this.ExpectedOutputStrings) - { - if (!standardOutputString.Contains(expectedString)) - { - errors.Add(String.Format("The text '{0}' was not found in stdout", expectedString)); - } - } - } - - // Verify that the expected regular expressions match stderr - if (null != this.ExpectedOutputRegexs) - { - foreach (Regex expectedRegex in this.ExpectedOutputRegexs) - { - if (!expectedRegex.IsMatch(standardOutputString)) - { - errors.Add(String.Format("Regex {0} did not match stdout", expectedRegex.ToString())); - } - } - } - - // Verify that the expected regular expressions match stdout - if (null != this.ExpectedErrorRegexs) - { - foreach (Regex expectedRegex in this.ExpectedErrorRegexs) - { - if (!expectedRegex.IsMatch(standardErrorString)) - { - errors.Add(String.Format("Regex {0} did not match stderr", expectedRegex.ToString())); - } - } - } - - return errors; - } - - /// - /// Clears all of the expected results and resets them to the default values - /// - public virtual void SetDefaultExpectedResults() - { - this.ExpectedErrorRegexs = new List(); - this.ExpectedErrorStrings = new List(); - this.ExpectedExitCode = null; - this.ExpectedOutputRegexs = new List(); - this.ExpectedOutputStrings = new List(); - } - - /// - /// Returns a string with data contained in the result. - /// - /// A string - private static string FormatResult(ExternalExecutableResult result) - { - var returnValue = new StringBuilder(); - returnValue.AppendLine(); - returnValue.AppendLine("----------------"); - returnValue.AppendLine("Tool run result:"); - returnValue.AppendLine("----------------"); - returnValue.AppendLine("Command:"); - returnValue.AppendLine($"\"{result.StartInfo.FileName}\" {result.StartInfo.Arguments}"); - returnValue.AppendLine(); - returnValue.AppendLine("Standard Output:"); - foreach (var line in result.StandardOutput ?? new string[0]) - { - returnValue.AppendLine(line); - } - returnValue.AppendLine("Standard Error:"); - foreach (var line in result.StandardError ?? new string[0]) - { - returnValue.AppendLine(line); - } - returnValue.AppendLine("Exit Code:"); - returnValue.AppendLine(Convert.ToString(result.ExitCode)); - returnValue.AppendLine("----------------"); - - return returnValue.ToString(); - } - } -} diff --git a/src/WixToolsetTest.BurnE2E/WixTestBase.cs b/src/WixToolsetTest.BurnE2E/WixTestBase.cs deleted file mode 100644 index 650b0681..00000000 --- a/src/WixToolsetTest.BurnE2E/WixTestBase.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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 Xunit.Abstractions; - - public abstract class WixTestBase - { - protected WixTestBase(ITestOutputHelper testOutputHelper) - { - this.TestContext = new WixTestContext(testOutputHelper); - } - - /// - /// The test context for the current test. - /// - public WixTestContext TestContext { get; } - } -} diff --git a/src/WixToolsetTest.BurnE2E/WixTestContext.cs b/src/WixToolsetTest.BurnE2E/WixTestContext.cs deleted file mode 100644 index 0ae99a77..00000000 --- a/src/WixToolsetTest.BurnE2E/WixTestContext.cs +++ /dev/null @@ -1,73 +0,0 @@ -// 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.IO; - using System.Linq; - using System.Reflection; - using Microsoft.Win32; - using WixBuildTools.TestSupport; - using Xunit.Abstractions; - - public class WixTestContext - { - static readonly string RootDataPath = Path.GetFullPath(TestData.Get("TestData")); - - public WixTestContext(ITestOutputHelper testOutputHelper) - { - var test = GetTest(testOutputHelper); - var splitClassName = test.TestCase.TestMethod.TestClass.Class.Name.Split('.'); - - this.TestGroupName = splitClassName.Last(); - this.TestName = test.TestCase.TestMethod.Method.Name; - - this.TestDataFolder = Path.Combine(RootDataPath, this.TestGroupName); - } - - public string TestDataFolder { get; } - - /// - /// Gets the name of the current test group. - /// - public string TestGroupName { get; } - - public string TestName { get; } - - /// - /// Gets the test install directory for the current test. - /// - /// Additional subdirectories under the test install directory. - /// Full path to the test install directory. - /// - /// The package or bundle must install into [ProgramFilesFolder]\~Test WiX\[TestGroupName]\([Additional]). - /// - public string GetTestInstallFolder(string additionalPath = null) - { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "~Test WiX", this.TestGroupName, additionalPath ?? String.Empty); - } - - /// - /// Gets the test registry key for the current test. - /// - /// Additional subkeys under the test registry key. - /// Full path to the test registry key. - /// - /// The package must write into HKLM\Software\WiX\Tests\[TestGroupName]\([Additional]). - /// - public RegistryKey GetTestRegistryRoot(string additionalPath = null) - { - var key = String.Format(@"Software\WOW6432Node\WiX\Tests\{0}\{1}", this.TestGroupName, additionalPath ?? String.Empty); - return Registry.LocalMachine.OpenSubKey(key, true); - } - - private static ITest GetTest(ITestOutputHelper output) - { - // https://github.com/xunit/xunit/issues/416#issuecomment-378512739 - var type = output.GetType(); - var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); - var test = (ITest)testMember.GetValue(output); - return test; - } - } -} diff --git a/src/WixToolsetTest.BurnE2E/WixToolsetTest.BurnE2E.csproj b/src/WixToolsetTest.BurnE2E/WixToolsetTest.BurnE2E.csproj index b3cf427f..5217826e 100644 --- a/src/WixToolsetTest.BurnE2E/WixToolsetTest.BurnE2E.csproj +++ b/src/WixToolsetTest.BurnE2E/WixToolsetTest.BurnE2E.csproj @@ -12,6 +12,10 @@ + + + + -- cgit v1.2.3-55-g6feb