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/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 + 11 files changed, 1687 insertions(+) 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 (limited to 'src/WixTestTools') 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 + + + + + + + + + + + + + + -- cgit v1.2.3-55-g6feb