aboutsummaryrefslogtreecommitdiff
path: root/src/WixTestTools
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixTestTools')
-rw-r--r--src/WixTestTools/BundleInstaller.cs141
-rw-r--r--src/WixTestTools/BundleRegistration.cs145
-rw-r--r--src/WixTestTools/BundleVerifier.cs96
-rw-r--r--src/WixTestTools/MSIExec.cs753
-rw-r--r--src/WixTestTools/MsiUtilities.cs27
-rw-r--r--src/WixTestTools/PackageInstaller.cs90
-rw-r--r--src/WixTestTools/PackageVerifier.cs77
-rw-r--r--src/WixTestTools/TestTool.cs245
-rw-r--r--src/WixTestTools/WixTestBase.cs19
-rw-r--r--src/WixTestTools/WixTestContext.cs73
-rw-r--r--src/WixTestTools/WixTestTools.csproj21
11 files changed, 1687 insertions, 0 deletions
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 @@
1// 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.
2
3namespace WixTestTools
4{
5 using System;
6 using System.IO;
7 using System.Text;
8
9 public partial class BundleInstaller : IDisposable
10 {
11 public BundleInstaller(WixTestContext testContext, string name)
12 {
13 this.Bundle = Path.Combine(testContext.TestDataFolder, $"{name}.exe");
14 this.BundlePdb = Path.Combine(testContext.TestDataFolder, $"{name}.wixpdb");
15 this.TestGroupName = testContext.TestGroupName;
16 this.TestName = testContext.TestName;
17 }
18
19 public string Bundle { get; }
20
21 public string TestGroupName { get; }
22
23 public string TestName { get; }
24
25 /// <summary>
26 /// Installs the bundle with optional arguments.
27 /// </summary>
28 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
29 /// <param name="arguments">Optional arguments to pass to the tool.</param>
30 /// <returns>Path to the generated log file.</returns>
31 public string Install(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
32 {
33 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Install, arguments);
34 }
35
36 /// <summary>
37 /// Modify the bundle with optional arguments.
38 /// </summary>
39 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
40 /// <param name="arguments">Optional arguments to pass to the tool.</param>
41 /// <returns>Path to the generated log file.</returns>
42 public string Modify(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
43 {
44 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Modify, arguments);
45 }
46
47 /// <summary>
48 /// Repairs the bundle with optional arguments.
49 /// </summary>
50 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
51 /// <param name="arguments">Optional arguments to pass to the tool.</param>
52 /// <returns>Path to the generated log file.</returns>
53 public string Repair(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
54 {
55 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Repair, arguments);
56 }
57
58 /// <summary>
59 /// Uninstalls the bundle with optional arguments.
60 /// </summary>
61 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
62 /// <param name="arguments">Optional arguments to pass to the tool.</param>
63 /// <returns>Path to the generated log file.</returns>
64 public string Uninstall(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
65 {
66 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Uninstall, arguments);
67 }
68
69 /// <summary>
70 /// Uninstalls the bundle at the given path with optional arguments.
71 /// </summary>
72 /// <param name="bundlePath">This should be the bundle in the package cache.</param>
73 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
74 /// <param name="arguments">Optional arguments to pass to the tool.</param>
75 /// <returns>Path to the generated log file.</returns>
76 public string Uninstall(string bundlePath, int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
77 {
78 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Uninstall, arguments, bundlePath: bundlePath);
79 }
80
81 /// <summary>
82 /// Executes the bundle with optional arguments.
83 /// </summary>
84 /// <param name="expectedExitCode">Expected exit code.</param>
85 /// <param name="mode">Install mode.</param>
86 /// <param name="arguments">Optional arguments to pass to the tool.</param>
87 /// <returns>Path to the generated log file.</returns>
88 private string RunBundleWithArguments(int expectedExitCode, MSIExec.MSIExecMode mode, string[] arguments, bool assertOnError = true, string bundlePath = null)
89 {
90 TestTool bundle = new TestTool(bundlePath ?? this.Bundle);
91 var sb = new StringBuilder();
92
93 // Be sure to run silent.
94 sb.Append(" -quiet");
95
96 // Generate the log file name.
97 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));
98 sb.AppendFormat(" -log \"{0}\"", logFile);
99
100 // Set operation.
101 switch (mode)
102 {
103 case MSIExec.MSIExecMode.Modify:
104 sb.Append(" -modify");
105 break;
106
107 case MSIExec.MSIExecMode.Repair:
108 sb.Append(" -repair");
109 break;
110
111 case MSIExec.MSIExecMode.Cleanup:
112 case MSIExec.MSIExecMode.Uninstall:
113 sb.Append(" -uninstall");
114 break;
115 }
116
117 // Add additional arguments.
118 if (null != arguments)
119 {
120 sb.Append(" ");
121 sb.Append(String.Join(" ", arguments));
122 }
123
124 // Set the arguments.
125 bundle.Arguments = sb.ToString();
126
127 // Run the tool and assert the expected code.
128 bundle.ExpectedExitCode = expectedExitCode;
129 bundle.Run(assertOnError);
130
131 // Return the log file name.
132 return logFile;
133 }
134
135 public void Dispose()
136 {
137 string[] args = { "-burn.ignoredependencies=ALL" };
138 this.RunBundleWithArguments((int)MSIExec.MSIExecReturnCode.SUCCESS, MSIExec.MSIExecMode.Cleanup, args, assertOnError: false);
139 }
140 }
141}
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 @@
1// 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.
2
3namespace WixTestTools
4{
5 using System;
6 using Microsoft.Win32;
7
8 public class BundleRegistration
9 {
10 public const string BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY = "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
11 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH = "BundleCachePath";
12 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_ADDON_CODE = "BundleAddonCode";
13 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_DETECT_CODE = "BundleDetectCode";
14 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_PATCH_CODE = "BundlePatchCode";
15 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE = "BundleUpgradeCode";
16 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_DISPLAY_NAME = "DisplayName";
17 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION = "BundleVersion";
18 public const string BURN_REGISTRATION_REGISTRY_ENGINE_VERSION = "EngineVersion";
19 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_PROVIDER_KEY = "BundleProviderKey";
20 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_TAG = "BundleTag";
21 public const string REGISTRY_REBOOT_PENDING_FORMAT = "{0}.RebootRequired";
22 public const string REGISTRY_BUNDLE_INSTALLED = "Installed";
23 public const string REGISTRY_BUNDLE_DISPLAY_ICON = "DisplayIcon";
24 public const string REGISTRY_BUNDLE_DISPLAY_VERSION = "DisplayVersion";
25 public const string REGISTRY_BUNDLE_ESTIMATED_SIZE = "EstimatedSize";
26 public const string REGISTRY_BUNDLE_PUBLISHER = "Publisher";
27 public const string REGISTRY_BUNDLE_HELP_LINK = "HelpLink";
28 public const string REGISTRY_BUNDLE_HELP_TELEPHONE = "HelpTelephone";
29 public const string REGISTRY_BUNDLE_URL_INFO_ABOUT = "URLInfoAbout";
30 public const string REGISTRY_BUNDLE_URL_UPDATE_INFO = "URLUpdateInfo";
31 public const string REGISTRY_BUNDLE_PARENT_DISPLAY_NAME = "ParentDisplayName";
32 public const string REGISTRY_BUNDLE_PARENT_KEY_NAME = "ParentKeyName";
33 public const string REGISTRY_BUNDLE_COMMENTS = "Comments";
34 public const string REGISTRY_BUNDLE_CONTACT = "Contact";
35 public const string REGISTRY_BUNDLE_NO_MODIFY = "NoModify";
36 public const string REGISTRY_BUNDLE_MODIFY_PATH = "ModifyPath";
37 public const string REGISTRY_BUNDLE_NO_ELEVATE_ON_MODIFY = "NoElevateOnModify";
38 public const string REGISTRY_BUNDLE_NO_REMOVE = "NoRemove";
39 public const string REGISTRY_BUNDLE_SYSTEM_COMPONENT = "SystemComponent";
40 public const string REGISTRY_BUNDLE_QUIET_UNINSTALL_STRING = "QuietUninstallString";
41 public const string REGISTRY_BUNDLE_UNINSTALL_STRING = "UninstallString";
42 public const string REGISTRY_BUNDLE_RESUME_COMMAND_LINE = "BundleResumeCommandLine";
43 public const string REGISTRY_BUNDLE_VERSION_MAJOR = "VersionMajor";
44 public const string REGISTRY_BUNDLE_VERSION_MINOR = "VersionMinor";
45
46 public string[] AddonCodes { get; set; }
47
48 public string CachePath { get; set; }
49
50 public string DisplayName { get; set; }
51
52 public string[] DetectCodes { get; set; }
53
54 public string EngineVersion { get; set; }
55
56 public int? EstimatedSize { get; set; }
57
58 public int? Installed { get; set; }
59
60 public string ModifyPath { get; set; }
61
62 public string[] PatchCodes { get; set; }
63
64 public string ProviderKey { get; set; }
65
66 public string Publisher { get; set; }
67
68 public string QuietUninstallString { get; set; }
69
70 public string QuietUninstallCommand { get; set; }
71
72 public string QuietUninstallCommandArguments { get; set; }
73
74 public string Tag { get; set; }
75
76 public string UninstallCommand { get; set; }
77
78 public string UninstallCommandArguments { get; set; }
79
80 public string UninstallString { get; set; }
81
82 public string[] UpgradeCodes { get; set; }
83
84 public string UrlInfoAbout { get; set; }
85
86 public string UrlUpdateInfo { get; set; }
87
88 public string Version { get; set; }
89
90 public static bool TryGetPerMachineBundleRegistrationById(string bundleId, out BundleRegistration registration)
91 {
92 var registrationKeyPath = $"{BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY}\\{bundleId}";
93 using var registrationKey = Registry.LocalMachine.OpenSubKey(registrationKeyPath);
94 var success = registrationKey != null;
95 registration = success ? GetBundleRegistration(registrationKey) : null;
96 return success;
97 }
98
99 private static BundleRegistration GetBundleRegistration(RegistryKey idKey)
100 {
101 var registration = new BundleRegistration();
102
103 registration.AddonCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_ADDON_CODE) as string[];
104 registration.CachePath = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH) as string;
105 registration.DetectCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_DETECT_CODE) as string[];
106 registration.PatchCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_PATCH_CODE) as string[];
107 registration.ProviderKey = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_PROVIDER_KEY) as string;
108 registration.Tag = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_TAG) as string;
109 registration.UpgradeCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE) as string[];
110 registration.Version = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION) as string;
111 registration.DisplayName = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_DISPLAY_NAME) as string;
112 registration.EngineVersion = idKey.GetValue(BURN_REGISTRATION_REGISTRY_ENGINE_VERSION) as string;
113 registration.EstimatedSize = idKey.GetValue(REGISTRY_BUNDLE_ESTIMATED_SIZE) as int?;
114 registration.Installed = idKey.GetValue(REGISTRY_BUNDLE_INSTALLED) as int?;
115 registration.ModifyPath = idKey.GetValue(REGISTRY_BUNDLE_MODIFY_PATH) as string;
116 registration.Publisher = idKey.GetValue(REGISTRY_BUNDLE_PUBLISHER) as string;
117 registration.UrlInfoAbout = idKey.GetValue(REGISTRY_BUNDLE_URL_INFO_ABOUT) as string;
118 registration.UrlUpdateInfo = idKey.GetValue(REGISTRY_BUNDLE_URL_UPDATE_INFO) as string;
119
120 registration.QuietUninstallString = idKey.GetValue(REGISTRY_BUNDLE_QUIET_UNINSTALL_STRING) as string;
121 if (!String.IsNullOrEmpty(registration.QuietUninstallString))
122 {
123 var closeQuote = registration.QuietUninstallString.IndexOf("\"", 1);
124 if (closeQuote > 0)
125 {
126 registration.QuietUninstallCommand = registration.QuietUninstallString.Substring(1, closeQuote - 1).Trim();
127 registration.QuietUninstallCommandArguments = registration.QuietUninstallString.Substring(closeQuote + 1).Trim();
128 }
129 }
130
131 registration.UninstallString = idKey.GetValue(REGISTRY_BUNDLE_UNINSTALL_STRING) as string;
132 if (!String.IsNullOrEmpty(registration.UninstallString))
133 {
134 var closeQuote = registration.UninstallString.IndexOf("\"", 1);
135 if (closeQuote > 0)
136 {
137 registration.UninstallCommand = registration.UninstallString.Substring(1, closeQuote - 1).Trim();
138 registration.UninstallCommandArguments = registration.UninstallString.Substring(closeQuote + 1).Trim();
139 }
140 }
141
142 return registration;
143 }
144 }
145}
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 @@
1// 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.
2
3namespace WixTestTools
4{
5 using System;
6 using System.IO;
7 using System.Linq;
8 using System.Text;
9 using Microsoft.Win32;
10 using WixToolset.Data;
11 using WixToolset.Data.Symbols;
12 using Xunit;
13
14 public partial class BundleInstaller
15 {
16 public const string FULL_BURN_POLICY_REGISTRY_PATH = "SOFTWARE\\WOW6432Node\\Policies\\WiX\\Burn";
17 public const string PACKAGE_CACHE_FOLDER_NAME = "Package Cache";
18
19 public string BundlePdb { get; }
20
21 private WixBundleSymbol BundleSymbol { get; set; }
22
23 private WixBundleSymbol GetBundleSymbol()
24 {
25 if (this.BundleSymbol == null)
26 {
27 using var wixOutput = WixOutput.Read(this.BundlePdb);
28 var intermediate = Intermediate.Load(wixOutput);
29 var section = intermediate.Sections.Single();
30 this.BundleSymbol = section.Symbols.OfType<WixBundleSymbol>().Single();
31 }
32
33 return this.BundleSymbol;
34 }
35
36 public string GetPackageCachePathForCacheId(string cacheId)
37 {
38 using var policyKey = Registry.LocalMachine.OpenSubKey(FULL_BURN_POLICY_REGISTRY_PATH);
39 var redirectedCachePath = policyKey?.GetValue("PackageCache") as string;
40 var cachePath = redirectedCachePath ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), PACKAGE_CACHE_FOLDER_NAME);
41 return Path.Combine(cachePath, cacheId);
42 }
43
44 public string GetExpectedCachedBundlePath()
45 {
46 var bundleSymbol = this.GetBundleSymbol();
47 var cachePath = this.GetPackageCachePathForCacheId(bundleSymbol.BundleId);
48 return Path.Combine(cachePath, Path.GetFileName(this.Bundle));
49 }
50
51 public bool TryGetPerMachineRegistration(out BundleRegistration registration)
52 {
53 var bundleSymbol = this.GetBundleSymbol();
54 var bundleId = bundleSymbol.BundleId;
55 return BundleRegistration.TryGetPerMachineBundleRegistrationById(bundleId, out registration);
56 }
57
58 public string VerifyRegisteredAndInPackageCache()
59 {
60 Assert.True(this.TryGetPerMachineRegistration(out var registration));
61
62 Assert.NotNull(registration.CachePath);
63 Assert.True(File.Exists(registration.CachePath));
64
65 var expectedCachePath = this.GetExpectedCachedBundlePath();
66 Assert.Equal(expectedCachePath, registration.CachePath, StringComparer.OrdinalIgnoreCase);
67
68 return registration.CachePath;
69 }
70
71 public void VerifyUnregisteredAndRemovedFromPackageCache()
72 {
73 var cachedBundlePath = this.GetExpectedCachedBundlePath();
74 this.VerifyUnregisteredAndRemovedFromPackageCache(cachedBundlePath);
75 }
76
77 public void VerifyUnregisteredAndRemovedFromPackageCache(string cachedBundlePath)
78 {
79 Assert.False(this.TryGetPerMachineRegistration(out _));
80 Assert.False(File.Exists(cachedBundlePath));
81 }
82
83 public void RemovePackageFromCache(string packageId)
84 {
85 using var wixOutput = WixOutput.Read(this.BundlePdb);
86 var intermediate = Intermediate.Load(wixOutput);
87 var section = intermediate.Sections.Single();
88 var packageSymbol = section.Symbols.OfType<WixBundlePackageSymbol>().Single(p => p.Id.Id == packageId);
89 var cachePath = this.GetPackageCachePathForCacheId(packageSymbol.CacheId);
90 if (Directory.Exists(cachePath))
91 {
92 Directory.Delete(cachePath, true);
93 }
94 }
95 }
96}
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 @@
1// 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.
2
3namespace WixTestTools
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using WixBuildTools.TestSupport;
9
10 public class MSIExec : TestTool
11 {
12 /// <summary>
13 /// The expected exit code of the tool
14 /// </summary>
15 public new MSIExecReturnCode ExpectedExitCode
16 {
17 get { return (MSIExecReturnCode)base.ExpectedExitCode; }
18 set { base.ExpectedExitCode = (int?)value; }
19 }
20
21 /// <summary>
22 /// Mode of execution (install, uninstall, or repair)
23 /// </summary>
24 public MSIExecMode ExecutionMode { get; set; }
25
26 /// <summary>
27 /// Path to msi or ProductCode
28 /// </summary>
29 public string Product { get; set; }
30
31 /// <summary>
32 /// Logging Options
33 /// </summary>
34 public MSIExecLoggingOptions LoggingOptions { get; set; }
35
36 /// <summary>
37 /// Path to the log file
38 /// </summary>
39 public string LogFile { get; set; }
40
41 /// <summary>
42 /// Unattended mode - progress bar only
43 /// </summary>
44 public bool Passive { get; set; }
45
46 /// <summary>
47 /// Quiet mode, no user interaction
48 /// </summary>
49 public bool Quiet { get; set; }
50
51 /// <summary>
52 /// Sets user interface level
53 /// </summary>
54 public MSIExecUserInterfaceLevel UserInterfaceLevel { get; set; }
55
56 /// <summary>
57 /// Do not restart after the installation is complete
58 /// </summary>
59 public bool NoRestart { get; set; }
60
61 /// <summary>
62 /// Prompts the user for restart if necessary
63 /// </summary>
64 public bool PromptRestart { get; set; }
65
66 /// <summary>
67 /// Always restart the computer after installation
68 /// </summary>
69 public bool ForceRestart { get; set; }
70
71 /// <summary>
72 /// Other arguments.
73 /// </summary>
74 public string OtherArguments { get; set; }
75
76 /// <summary>
77 /// Constructor that uses the default location for MSIExec.
78 /// </summary>
79 public MSIExec()
80 : this(Environment.SystemDirectory)
81 {
82 }
83
84 /// <summary>
85 /// Constructor that accepts a path to the MSIExec location.
86 /// </summary>
87 /// <param name="toolDirectory">The directory of MSIExec.exe.</param>
88 public MSIExec(string toolDirectory)
89 : base(Path.Combine(toolDirectory, "MSIExec.exe"))
90 {
91 this.SetDefaultArguments();
92 }
93
94 public override ExternalExecutableResult Run(bool assertOnError)
95 {
96 this.Arguments = this.GetArguments();
97 return base.Run(assertOnError);
98 }
99
100 /// <summary>
101 /// Clears all of the assigned arguments and resets them to the default values.
102 /// </summary>
103 public void SetDefaultArguments()
104 {
105 this.ExecutionMode = MSIExecMode.Install;
106 this.Product = String.Empty;
107 this.Quiet = true;
108 this.Passive = false;
109 this.UserInterfaceLevel = MSIExecUserInterfaceLevel.None;
110 this.NoRestart = true;
111 this.ForceRestart = false;
112 this.PromptRestart = false;
113 this.LogFile = string.Empty;
114 this.LoggingOptions = MSIExecLoggingOptions.VOICEWARMUP;
115 this.OtherArguments = String.Empty;
116 }
117
118 public string GetArguments()
119 {
120 var arguments = new StringBuilder();
121
122 // quiet
123 if (this.Quiet)
124 {
125 arguments.Append(" /quiet ");
126 }
127
128 // passive
129 if (this.Passive)
130 {
131 arguments.Append(" /passive ");
132 }
133
134 // UserInterfaceLevel
135 switch (this.UserInterfaceLevel)
136 {
137 case MSIExecUserInterfaceLevel.None:
138 arguments.Append(" /qn ");
139 break;
140 case MSIExecUserInterfaceLevel.Basic:
141 arguments.Append(" /qb ");
142 break;
143 case MSIExecUserInterfaceLevel.Reduced:
144 arguments.Append(" /qr ");
145 break;
146 case MSIExecUserInterfaceLevel.Full:
147 arguments.Append(" /qf ");
148 break;
149 }
150
151 // NoRestart
152 if (this.NoRestart)
153 {
154 arguments.Append(" /norestart ");
155 }
156
157 // PromptRestart
158 if (this.PromptRestart)
159 {
160 arguments.Append(" /promptrestart ");
161 }
162
163 // ForceRestart
164 if (this.ForceRestart)
165 {
166 arguments.Append(" /forcerestart ");
167 }
168
169 // Logging options
170 var loggingOptionsString = new StringBuilder();
171 if ((this.LoggingOptions & MSIExecLoggingOptions.Status_Messages) == MSIExecLoggingOptions.Status_Messages)
172 {
173 loggingOptionsString.Append("i");
174 }
175 if ((this.LoggingOptions & MSIExecLoggingOptions.Nonfatal_Warnings) == MSIExecLoggingOptions.Nonfatal_Warnings)
176 {
177 loggingOptionsString.Append("w");
178 }
179 if ((this.LoggingOptions & MSIExecLoggingOptions.All_Error_Messages) == MSIExecLoggingOptions.All_Error_Messages)
180 {
181 loggingOptionsString.Append("e");
182 }
183 if ((this.LoggingOptions & MSIExecLoggingOptions.Start_Up_Of_Actions) == MSIExecLoggingOptions.Start_Up_Of_Actions)
184 {
185 loggingOptionsString.Append("a");
186 }
187 if ((this.LoggingOptions & MSIExecLoggingOptions.Action_Specific_Records) == MSIExecLoggingOptions.Action_Specific_Records)
188 {
189 loggingOptionsString.Append("r");
190 }
191 if ((this.LoggingOptions & MSIExecLoggingOptions.User_Requests) == MSIExecLoggingOptions.User_Requests)
192 {
193 loggingOptionsString.Append("u");
194 }
195 if ((this.LoggingOptions & MSIExecLoggingOptions.Initial_UI_Parameters) == MSIExecLoggingOptions.Initial_UI_Parameters)
196 {
197 loggingOptionsString.Append("c");
198 }
199 if ((this.LoggingOptions & MSIExecLoggingOptions.OutOfMemory_Or_Fatal_Exit_Information) == MSIExecLoggingOptions.OutOfMemory_Or_Fatal_Exit_Information)
200 {
201 loggingOptionsString.Append("m");
202 }
203 if ((this.LoggingOptions & MSIExecLoggingOptions.OutOfDiskSpace_Messages) == MSIExecLoggingOptions.OutOfDiskSpace_Messages)
204 {
205 loggingOptionsString.Append("o");
206 }
207 if ((this.LoggingOptions & MSIExecLoggingOptions.Terminal_Properties) == MSIExecLoggingOptions.Terminal_Properties)
208 {
209 loggingOptionsString.Append("p");
210 }
211 if ((this.LoggingOptions & MSIExecLoggingOptions.Verbose_Output) == MSIExecLoggingOptions.Verbose_Output)
212 {
213 loggingOptionsString.Append("v");
214 }
215 if ((this.LoggingOptions & MSIExecLoggingOptions.Extra_Debugging_Information) == MSIExecLoggingOptions.Extra_Debugging_Information)
216 {
217 loggingOptionsString.Append("x");
218 }
219 if ((this.LoggingOptions & MSIExecLoggingOptions.Append_To_Existing_Log_File) == MSIExecLoggingOptions.Append_To_Existing_Log_File)
220 {
221 loggingOptionsString.Append("+");
222 }
223 if ((this.LoggingOptions & MSIExecLoggingOptions.Flush_Each_line) == MSIExecLoggingOptions.Flush_Each_line)
224 {
225 loggingOptionsString.Append("!");
226 }
227 if ((this.LoggingOptions & MSIExecLoggingOptions.Log_All_Information) == MSIExecLoggingOptions.Log_All_Information)
228 {
229 loggingOptionsString.Append("*");
230 }
231
232 // logfile and logging options
233 if (0 != loggingOptionsString.Length || !string.IsNullOrEmpty(this.LogFile))
234 {
235 arguments.Append(" /l");
236 if (0 != loggingOptionsString.Length)
237 {
238 arguments.AppendFormat("{0} ", loggingOptionsString);
239 }
240 if (!string.IsNullOrEmpty(this.LogFile))
241 {
242 arguments.AppendFormat(" \"{0}\" ", this.LogFile);
243 }
244 }
245
246 // OtherArguments
247 if (!String.IsNullOrEmpty(this.OtherArguments))
248 {
249 arguments.AppendFormat(" {0} ", this.OtherArguments);
250 }
251
252 // execution mode
253 switch (this.ExecutionMode)
254 {
255 case MSIExecMode.Install:
256 arguments.Append(" /package ");
257 break;
258 case MSIExecMode.AdministrativeInstall:
259 arguments.Append(" /a ");
260 break;
261 case MSIExecMode.Repair:
262 arguments.Append(" /f ");
263 break;
264 case MSIExecMode.Cleanup:
265 case MSIExecMode.Uninstall:
266 arguments.Append(" /uninstall ");
267 break;
268 };
269
270 // product
271 if (!string.IsNullOrEmpty(this.Product))
272 {
273 arguments.AppendFormat(" \"{0}\" ", this.Product);
274 }
275
276 return arguments.ToString();
277 }
278
279 /// <summary>
280 /// Return codes from an MSI install or uninstall
281 /// </summary>
282 /// <remarks>
283 /// Error codes indicative of success are:
284 /// ERROR_SUCCESS, ERROR_SUCCESS_REBOOT_INITIATED, and ERROR_SUCCESS_REBOOT_REQUIRED
285 /// </remarks>
286 public enum MSIExecReturnCode
287 {
288 /// <summary>
289 /// ERROR_SUCCESS 0
290 /// Action completed successfully.
291 /// </summary>
292 SUCCESS = 0,
293
294 /// <summary>
295 /// ERROR_INVALID_DATA 13
296 /// The data is invalid.
297 /// </summary>
298 ERROR_INVALID_DATA = 13,
299
300 /// <summary>
301 /// ERROR_INVALID_PARAMETER 87
302 /// One of the parameters was invalid.
303 /// </summary>
304 ERROR_INVALID_PARAMETER = 87,
305
306 /// <summary>
307 /// ERROR_CALL_NOT_IMPLEMENTED 120
308 /// This value is returned when a custom action attempts to call a function that cannot be called from custom actions.
309 /// The function returns the value ERROR_CALL_NOT_IMPLEMENTED. Available beginning with Windows Installer version 3.0.
310 /// </summary>
311 ERROR_CALL_NOT_IMPLEMENTED = 120,
312
313 /// <summary>
314 /// ERROR_APPHELP_BLOCK 1259
315 /// If Windows Installer determines a product may be incompatible with the current operating system,
316 /// it displays a dialog box informing the user and asking whether to try to install anyway.
317 /// This error code is returned if the user chooses not to try the installation.
318 /// </summary>
319 ERROR_APPHELP_BLOCK = 1259,
320
321 /// <summary>
322 /// ERROR_INSTALL_SERVICE_FAILURE 1601
323 /// The Windows Installer service could not be accessed.
324 /// Contact your support personnel to verify that the Windows Installer service is properly registered.
325 /// </summary>
326 ERROR_INSTALL_SERVICE_FAILURE = 1601,
327
328
329 /// <summary>
330 /// ERROR_INSTALL_USEREXIT 1602
331 /// The user cancels installation.
332 /// </summary>
333 ERROR_INSTALL_USEREXIT = 1602,
334
335 /// <summary>
336 /// ERROR_INSTALL_FAILURE 1603
337 /// A fatal error occurred during installation.
338 /// </summary>
339 ERROR_INSTALL_FAILURE = 1603,
340
341 /// <summary>
342 /// ERROR_INSTALL_SUSPEND 1604
343 /// Installation suspended, incomplete.
344 /// </summary>
345 ERROR_INSTALL_SUSPEND = 1604,
346
347 /// <summary>
348 /// ERROR_UNKNOWN_PRODUCT 1605
349 /// This action is only valid for products that are currently installed.
350 /// </summary>
351 ERROR_UNKNOWN_PRODUCT = 1605,
352
353 /// <summary>
354 /// ERROR_UNKNOWN_FEATURE 1606
355 /// The feature identifier is not registered.
356 /// </summary>
357 ERROR_UNKNOWN_FEATURE = 1606,
358
359 /// <summary>
360 /// ERROR_UNKNOWN_COMPONENT 1607
361 /// The component identifier is not registered.
362 /// </summary>
363 ERROR_UNKNOWN_COMPONENT = 1607,
364
365 /// <summary>
366 /// ERROR_UNKNOWN_PROPERTY 1608
367 /// This is an unknown property.
368 /// </summary>
369 ERROR_UNKNOWN_PROPERTY = 1608,
370
371 /// <summary>
372 /// ERROR_INVALID_HANDLE_STATE 1609
373 /// The handle is in an invalid state.
374 /// </summary>
375 ERROR_INVALID_HANDLE_STATE = 1609,
376
377 /// <summary>
378 /// ERROR_BAD_CONFIGURATION 1610
379 /// The configuration data for this product is corrupt. Contact your support personnel.
380 /// </summary>
381 ERROR_BAD_CONFIGURATION = 1610,
382
383 /// <summary>
384 /// ERROR_INDEX_ABSENT 1611
385 /// The component qualifier not present.
386 /// </summary>
387 ERROR_INDEX_ABSENT = 1611,
388
389 /// <summary>ERROR_INSTALL_SOURCE_ABSENT 1612
390 /// The installation source for this product is not available.
391 /// Verify that the source exists and that you can access it.
392 /// </summary>
393 ERROR_INSTALL_SOURCE_ABSENT = 1612,
394
395 /// <summary>
396 /// ERROR_INSTALL_PACKAGE_VERSION 1613
397 /// This installation package cannot be installed by the Windows Installer service.
398 /// You must install a Windows service pack that contains a newer version of the Windows Installer service.
399 /// </summary>
400 ERROR_INSTALL_PACKAGE_VERSION = 1613,
401
402 /// <summary>
403 /// ERROR_PRODUCT_UNINSTALLED 1614
404 /// The product is uninstalled.
405 /// </summary>
406 ERROR_PRODUCT_UNINSTALLED = 1614,
407
408 /// <summary>
409 /// ERROR_BAD_QUERY_SYNTAX 1615
410 /// The SQL query syntax is invalid or unsupported.
411 /// </summary>
412 ERROR_BAD_QUERY_SYNTAX = 1615,
413
414 /// <summary>
415 /// ERROR_INVALID_FIELD 1616
416 /// The record field does not exist.
417 /// </summary>
418 ERROR_INVALID_FIELD = 1616,
419
420 /// <summary>
421 /// ERROR_INSTALL_ALREADY_RUNNING 1618
422 /// Another installation is already in progress. Complete that installation before proceeding with this install.
423 /// For information about the mutex, see _MSIExecute Mutex.
424 /// </summary>
425 ERROR_INSTALL_ALREADY_RUNNING = 1618,
426
427 /// <summary>
428 /// ERROR_INSTALL_PACKAGE_OPEN_FAILED 1619
429 /// This installation package could not be opened. Verify that the package exists and is accessible, or contact the
430 /// application vendor to verify that this is a valid Windows Installer package.
431 /// </summary>
432 ERROR_INSTALL_PACKAGE_OPEN_FAILED = 1619,
433
434
435 /// <summary>
436 /// ERROR_INSTALL_PACKAGE_INVALID 1620
437 /// This installation package could not be opened.
438 /// Contact the application vendor to verify that this is a valid Windows Installer package.
439 /// </summary>
440 ERROR_INSTALL_PACKAGE_INVALID = 1620,
441
442 /// <summary>
443 /// ERROR_INSTALL_UI_FAILURE 1621
444 /// There was an error starting the Windows Installer service user interface.
445 /// Contact your support personnel.
446 /// </summary>
447 ERROR_INSTALL_UI_FAILURE = 1621,
448
449 /// <summary>
450 /// ERROR_INSTALL_LOG_FAILURE 1622
451 /// There was an error opening installation log file.
452 /// Verify that the specified log file location exists and is writable.
453 /// </summary>
454 ERROR_INSTALL_LOG_FAILURE = 1622,
455
456 /// <summary>
457 /// ERROR_INSTALL_LANGUAGE_UNSUPPORTED 1623
458 /// This language of this installation package is not supported by your system.
459 /// </summary>
460 ERROR_INSTALL_LANGUAGE_UNSUPPORTED = 1623,
461
462 /// <summary>
463 /// ERROR_INSTALL_TRANSFORM_FAILURE 1624
464 /// There was an error applying transforms.
465 /// Verify that the specified transform paths are valid.
466 /// </summary>
467 ERROR_INSTALL_TRANSFORM_FAILURE = 1624,
468
469
470 /// <summary>
471 /// ERROR_INSTALL_PACKAGE_REJECTED 1625
472 /// This installation is forbidden by system policy.
473 /// Contact your system administrator.
474 /// </summary>
475 ERROR_INSTALL_PACKAGE_REJECTED = 1625,
476
477 /// <summary>
478 /// ERROR_FUNCTION_NOT_CALLED 1626
479 /// The function could not be executed.
480 /// </summary>
481 ERROR_FUNCTION_NOT_CALLED = 1626,
482
483 /// <summary>
484 /// ERROR_FUNCTION_FAILED 1627
485 /// The function failed during execution.
486 /// </summary>
487 ERROR_FUNCTION_FAILED = 1627,
488
489 /// <summary>
490 /// ERROR_INVALID_TABLE 1628
491 /// An invalid or unknown table was specified.
492 /// </summary>
493 ERROR_INVALID_TABLE = 1628,
494
495 /// <summary>
496 /// ERROR_DATATYPE_MISMATCH 1629
497 /// The data supplied is the wrong type.
498 /// </summary>
499 ERROR_DATATYPE_MISMATCH = 1629,
500
501 /// <summary>
502 /// ERROR_UNSUPPORTED_TYPE 1630
503 /// Data of this type is not supported.
504 /// </summary>
505 ERROR_UNSUPPORTED_TYPE = 1630,
506
507 /// <summary>
508 /// ERROR_CREATE_FAILED 1631
509 /// The Windows Installer service failed to start.
510 /// Contact your support personnel.
511 /// </summary>
512 ERROR_CREATE_FAILED = 1631,
513
514 /// <summary>
515 /// ERROR_INSTALL_TEMP_UNWRITABLE 1632
516 /// The Temp folder is either full or inaccessible.
517 /// Verify that the Temp folder exists and that you can write to it.
518 /// </summary>
519 ERROR_INSTALL_TEMP_UNWRITABLE = 1632,
520
521 /// <summary>
522 /// ERROR_INSTALL_PLATFORM_UNSUPPORTED 1633
523 /// This installation package is not supported on this platform. Contact your application vendor. </summary>
524 ERROR_INSTALL_PLATFORM_UNSUPPORTED = 1633,
525
526 /// <summary>
527 /// ERROR_INSTALL_NOTUSED 1634
528 /// Component is not used on this machine.
529 /// </summary>
530 ERROR_INSTALL_NOTUSED = 1634,
531
532 /// <summary>
533 /// ERROR_PATCH_PACKAGE_OPEN_FAILED 1635
534 /// This patch package could not be opened. Verify that the patch package exists and is accessible,
535 /// or contact the application vendor to verify that this is a valid Windows Installer patch package.
536 /// </summary>
537 ERROR_PATCH_PACKAGE_OPEN_FAILED = 1635,
538
539 /// <summary>
540 /// ERROR_PATCH_PACKAGE_INVALID 1636
541 /// This patch package could not be opened.
542 /// Contact the application vendor to verify that this is a valid Windows Installer patch package.
543 /// </summary>
544 ERROR_PATCH_PACKAGE_INVALID = 1636,
545
546 /// <summary>
547 /// ERROR_PATCH_PACKAGE_UNSUPPORTED 1637
548 /// This patch package cannot be processed by the Windows Installer service.
549 /// You must install a Windows service pack that contains a newer version of the Windows Installer service.
550 /// </summary>
551 ERROR_PATCH_PACKAGE_UNSUPPORTED = 1637,
552
553 /// <summary>
554 /// ERROR_PRODUCT_VERSION 1638
555 /// Another version of this product is already installed.
556 /// Installation of this version cannot continue. To configure or remove the existing version of this product,
557 /// use Add/Remove Programs in Control Panel.
558 /// </summary>
559 ERROR_PRODUCT_VERSION = 1638,
560
561 /// <summary>
562 /// ERROR_INVALID_COMMAND_LINE 1639
563 /// Invalid command line argument.
564 /// Consult the Windows Installer SDK for detailed command-line help.
565 /// </summary>
566 ERROR_INVALID_COMMAND_LINE = 1639,
567
568 /// <summary>
569 /// ERROR_INSTALL_REMOTE_DISALLOWED 1640
570 /// The current user is not permitted to perform installations from a client session of a server running the
571 /// Terminal Server role service.
572 /// </summary>
573 ERROR_INSTALL_REMOTE_DISALLOWED = 1640,
574
575 /// <summary>
576 /// ERROR_SUCCESS_REBOOT_INITIATED 1641
577 /// The installer has initiated a restart.
578 /// This message is indicative of a success.
579 /// </summary>
580 ERROR_SUCCESS_REBOOT_INITIATED = 1641,
581
582 /// <summary>
583 /// ERROR_PATCH_TARGET_NOT_FOUND 1642
584 /// The installer cannot install the upgrade patch because the program being upgraded may be missing or the
585 /// upgrade patch updates a different version of the program.
586 /// Verify that the program to be upgraded exists on your computer and that you have the correct upgrade patch.
587 /// </summary>
588 ERROR_PATCH_TARGET_NOT_FOUND = 1642,
589
590 /// <summary>
591 /// ERROR_PATCH_PACKAGE_REJECTED 1643
592 /// The patch package is not permitted by system policy.
593 /// </summary>
594 ERROR_PATCH_PACKAGE_REJECTED = 1643,
595
596 /// <summary>
597 /// ERROR_INSTALL_TRANSFORM_REJECTED 1644
598 /// One or more customizations are not permitted by system policy.
599 /// </summary>
600 ERROR_INSTALL_TRANSFORM_REJECTED = 1644,
601
602 /// <summary>
603 /// ERROR_INSTALL_REMOTE_PROHIBITED 1645
604 /// Windows Installer does not permit installation from a Remote Desktop Connection.
605 /// </summary>
606 ERROR_INSTALL_REMOTE_PROHIBITED = 1645,
607
608 /// <summary>
609 /// ERROR_PATCH_REMOVAL_UNSUPPORTED 1646
610 /// The patch package is not a removable patch package. Available beginning with Windows Installer version 3.0.
611 /// </summary>
612 ERROR_PATCH_REMOVAL_UNSUPPORTED = 1646,
613
614 /// <summary>
615 /// ERROR_UNKNOWN_PATCH 1647
616 /// The patch is not applied to this product. Available beginning with Windows Installer version 3.0.
617 /// </summary>
618 ERROR_UNKNOWN_PATCH = 1647,
619
620 /// <summary>
621 /// ERROR_PATCH_NO_SEQUENCE 1648
622 /// No valid sequence could be found for the set of patches. Available beginning with Windows Installer version 3.0.
623 /// </summary>
624 ERROR_PATCH_NO_SEQUENCE = 1648,
625
626 /// <summary>
627 /// ERROR_PATCH_REMOVAL_DISALLOWED 1649
628 /// Patch removal was disallowed by policy. Available beginning with Windows Installer version 3.0. </summary>
629 ERROR_PATCH_REMOVAL_DISALLOWED = 1649,
630
631 /// <summary>
632 /// ERROR_INVALID_PATCH_XML = 1650
633 /// The XML patch data is invalid. Available beginning with Windows Installer version 3.0.
634 /// </summary>
635 ERROR_INVALID_PATCH_XML = 1650,
636
637 /// <summary>
638 /// ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT 1651
639 /// Administrative user failed to apply patch for a per-user managed or a per-machine application that is in advertise state.
640 /// Available beginning with Windows Installer version 3.0. </summary>
641 ERROR_PATCH_MANAGED_ADVERTISED_PRODUCT = 1651,
642
643 /// <summary>
644 /// ERROR_INSTALL_SERVICE_SAFEBOOT 1652
645 /// Windows Installer is not accessible when the computer is in Safe Mode.
646 /// Exit Safe Mode and try again or try using System Restore to return your computer to a previous state.
647 /// Available beginning with Windows Installer version 4.0.
648 /// </summary>
649 ERROR_INSTALL_SERVICE_SAFEBOOT = 1652,
650
651 /// <summary>
652 /// ERROR_ROLLBACK_DISABLED 1653
653 /// Could not perform a multiple-package transaction because rollback has been disabled.
654 /// Multiple-Package Installations cannot run if rollback is disabled. Available beginning with Windows Installer version 4.5.
655 /// </summary>
656 ERROR_ROLLBACK_DISABLED = 1653,
657
658 /// <summary>
659 /// ERROR_SUCCESS_REBOOT_REQUIRED 3010
660 /// A restart is required to complete the install. This message is indicative of a success.
661 /// This does not include installs where the ForceReboot action is run.
662 /// </summary>
663 ERROR_SUCCESS_REBOOT_REQUIRED = 3010
664 }
665
666 /// <summary>
667 /// Modes of operations for MSIExec; install, administrator install, uninstall .. etc
668 /// </summary>
669 public enum MSIExecMode
670 {
671 /// <summary>
672 /// Installs or configures a product
673 /// </summary>
674 Install = 0,
675
676 /// <summary>
677 /// Administrative install - Installs a product on the network
678 /// </summary>
679 AdministrativeInstall,
680
681 /// <summary>
682 /// Uninstalls the product
683 /// </summary>
684 Uninstall,
685
686 /// <summary>
687 /// Repairs a product
688 /// </summary>
689 Repair,
690
691 /// <summary>
692 /// Modifies a product
693 /// </summary>
694 Modify,
695
696 /// <summary>
697 /// Uninstalls the product as part of cleanup
698 /// </summary>
699 Cleanup,
700 }
701
702 /// <summary>
703 /// User interfave levels
704 /// </summary>
705 public enum MSIExecUserInterfaceLevel
706 {
707 /// <summary>
708 /// No UI
709 /// </summary>
710 None = 0,
711
712 /// <summary>
713 /// Basic UI
714 /// </summary>
715 Basic,
716
717 /// <summary>
718 /// Reduced UI
719 /// </summary>
720 Reduced,
721
722 /// <summary>
723 /// Full UI (default)
724 /// </summary>
725 Full
726 }
727
728 /// <summary>
729 /// Logging options
730 /// </summary>
731 [Flags]
732 public enum MSIExecLoggingOptions
733 {
734 Status_Messages = 0x0001,
735 Nonfatal_Warnings = 0x0002,
736 All_Error_Messages = 0x0004,
737 Start_Up_Of_Actions = 0x0008,
738 Action_Specific_Records = 0x0010,
739 User_Requests = 0x0020,
740 Initial_UI_Parameters = 0x0040,
741 OutOfMemory_Or_Fatal_Exit_Information = 0x0080,
742 OutOfDiskSpace_Messages = 0x0100,
743 Terminal_Properties = 0x0200,
744 Verbose_Output = 0x0400,
745 Append_To_Existing_Log_File = 0x0800,
746
747 Flush_Each_line = 0x1000,
748 Extra_Debugging_Information = 0x2000,
749 Log_All_Information = 0x4000,
750 VOICEWARMUP = 0x0FFF
751 }
752 }
753}
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 @@
1// 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.
2
3namespace WixTestTools
4{
5 using WixToolset.Dtf.WindowsInstaller;
6
7 public class MsiUtilities
8 {
9 /// <summary>
10 /// Return true if it finds the given productcode in system otherwise it returns false
11 /// </summary>
12 /// <param name="prodCode"></param>
13 /// <returns></returns>
14 public static bool IsProductInstalled(string prodCode)
15 {
16 //look in all user's products (both per-machine and per-user)
17 foreach (ProductInstallation product in ProductInstallation.GetProducts(null, "s-1-1-0", UserContexts.All))
18 {
19 if (product.ProductCode == prodCode)
20 {
21 return true;
22 }
23 }
24 return false;
25 }
26 }
27}
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 @@
1// 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.
2
3namespace WixTestTools
4{
5 using System;
6 using System.IO;
7 using static WixTestTools.MSIExec;
8
9 public partial class PackageInstaller : IDisposable
10 {
11 public PackageInstaller(WixTestContext testContext, string filename)
12 {
13 this.Package = Path.Combine(testContext.TestDataFolder, $"{filename}.msi");
14 this.PackagePdb = Path.Combine(testContext.TestDataFolder, $"{filename}.wixpdb");
15 this.TestContext = testContext;
16 }
17
18 public string Package { get; }
19
20 private WixTestContext TestContext { get; }
21
22 public string TestGroupName => this.TestContext.TestGroupName;
23
24 public string TestName => this.TestContext.TestName;
25
26 /// <summary>
27 /// Installs a .msi file
28 /// </summary>
29 /// <param name="expectedExitCode">Expected exit code</param>
30 /// <param name="otherArguments">Other arguments to pass to MSIExec.</param>
31 /// <returns>MSIExec log File</returns>
32 public string InstallProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments)
33 {
34 return this.RunMSIExec(MSIExecMode.Install, otherArguments, expectedExitCode);
35 }
36
37 /// <summary>
38 /// Uninstalls a .msi file
39 /// </summary>
40 /// <param name="expectedExitCode">Expected exit code</param>
41 /// <param name="otherArguments">Other arguments to pass to MSIExec.</param>
42 /// <returns>MSIExec log File</returns>
43 public string UninstallProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments)
44 {
45 return this.RunMSIExec(MSIExecMode.Uninstall, otherArguments, expectedExitCode);
46 }
47
48 /// <summary>
49 /// Repairs a .msi file
50 /// </summary>
51 /// <param name="expectedExitCode">Expected exit code</param>
52 /// <param name="otherArguments">Other arguments to pass to msiexe.exe.</param>
53 /// <returns>MSIExec log File</returns>
54 public string RepairProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments)
55 {
56 return this.RunMSIExec(MSIExecMode.Repair, otherArguments, expectedExitCode);
57 }
58
59 /// <summary>
60 /// Executes MSIExec on a .msi file
61 /// </summary>
62 /// <param name="mode">Mode of execution for MSIExec</param>
63 /// <param name="otherArguments">Other arguments to pass to MSIExec.</param>
64 /// <param name="expectedExitCode">Expected exit code</param>
65 /// <returns>MSIExec exit code</returns>
66 private string RunMSIExec(MSIExecMode mode, string[] otherArguments, MSIExecReturnCode expectedExitCode, bool assertOnError = true)
67 {
68 // Generate the log file name.
69 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));
70
71 var msiexec = new MSIExec
72 {
73 Product = this.Package,
74 ExecutionMode = mode,
75 OtherArguments = null != otherArguments ? String.Join(" ", otherArguments) : null,
76 ExpectedExitCode = expectedExitCode,
77 LogFile = logFile,
78 };
79
80 msiexec.Run(assertOnError);
81 return msiexec.LogFile;
82 }
83
84 public void Dispose()
85 {
86 string[] args = { "IGNOREDEPENDENCIES=ALL", "WIXFAILWHENDEFERRED=0" };
87 this.RunMSIExec(MSIExecMode.Cleanup, args, MSIExecReturnCode.SUCCESS, assertOnError: false);
88 }
89 }
90}
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 @@
1// 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.
2
3namespace WixTestTools
4{
5 using System;
6 using System.IO;
7 using System.Linq;
8 using WixToolset.Data;
9 using WixToolset.Data.WindowsInstaller;
10 using WixToolset.Data.WindowsInstaller.Rows;
11 using Xunit;
12
13 public partial class PackageInstaller
14 {
15 public string PackagePdb { get; }
16
17 private WindowsInstallerData WiData { get; set; }
18
19 public string GetInstalledFilePath(string filename)
20 {
21 return this.TestContext.GetTestInstallFolder(Path.Combine(this.GetInstallFolderName(), filename));
22 }
23
24 private WindowsInstallerData GetWindowsInstallerData()
25 {
26 if (this.WiData == null)
27 {
28 using var wixOutput = WixOutput.Read(this.PackagePdb);
29 this.WiData = WindowsInstallerData.Load(wixOutput);
30 }
31
32 return this.WiData;
33 }
34
35 public string GetInstallFolderName()
36 {
37 var wiData = this.GetWindowsInstallerData();
38 var row = wiData.Tables["Directory"].Rows.Single(r => r.FieldAsString(0) == "INSTALLFOLDER");
39 var value = row.FieldAsString(2);
40 var longNameIndex = value.IndexOf('|') + 1;
41 if (longNameIndex > 0)
42 {
43 return value.Substring(longNameIndex);
44 }
45 return value;
46 }
47
48 public string GetProperty(string name)
49 {
50 var wiData = this.GetWindowsInstallerData();
51 var row = wiData.Tables["Property"].Rows.Cast<PropertyRow>().Single(r => r.Property == name);
52 return row.Value;
53 }
54
55 public void VerifyInstalled(bool installed)
56 {
57 var productCode = this.GetProperty("ProductCode");
58 Assert.Equal(installed, MsiUtilities.IsProductInstalled(productCode));
59 }
60
61 public void VerifyTestRegistryRootDeleted()
62 {
63 using var testRegistryRoot = this.TestContext.GetTestRegistryRoot();
64 Assert.Null(testRegistryRoot);
65 }
66
67 public void VerifyTestRegistryValue(string name, string expectedValue)
68 {
69 using (var root = this.TestContext.GetTestRegistryRoot())
70 {
71 Assert.NotNull(root);
72 var actualValue = root.GetValue(name) as string;
73 Assert.Equal(expectedValue, actualValue);
74 }
75 }
76 }
77}
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 @@
1// 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.
2
3namespace WixTestTools
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Text;
8 using System.Text.RegularExpressions;
9 using WixBuildTools.TestSupport;
10 using Xunit;
11
12 public class TestTool : ExternalExecutable
13 {
14 /// <summary>
15 /// Constructor for a TestTool
16 /// </summary>
17 public TestTool()
18 : this(null)
19 {
20 }
21
22 /// <summary>
23 /// Constructor for a TestTool
24 /// </summary>
25 /// <param name="toolFile">The full path to the tool. Eg. c:\bin\candle.exe</param>
26 public TestTool(string toolFile)
27 : base(toolFile)
28 {
29 this.PrintOutputToConsole = true;
30 }
31
32 /// <summary>
33 /// The arguments to pass to the tool
34 /// </summary>
35 public virtual string Arguments { get; set; }
36
37 /// <summary>
38 /// Stores the errors that occurred when a run was checked against its expected results
39 /// </summary>
40 public List<string> Errors { get; set; }
41
42 /// <summary>
43 /// A list of Regex's that are expected to match stderr
44 /// </summary>
45 public List<Regex> ExpectedErrorRegexs { get; set; } = new List<Regex>();
46
47 /// <summary>
48 /// The expected error strings to stderr
49 /// </summary>
50 public List<string> ExpectedErrorStrings { get; set; } = new List<string>();
51
52 /// <summary>
53 /// The expected exit code of the tool
54 /// </summary>
55 public int? ExpectedExitCode { get; set; }
56
57 /// <summary>
58 /// A list of Regex's that are expected to match stdout
59 /// </summary>
60 public List<Regex> ExpectedOutputRegexs { get; set; } = new List<Regex>();
61
62 /// <summary>
63 /// The expected output strings to stdout
64 /// </summary>
65 public List<string> ExpectedOutputStrings { get; set; } = new List<string>();
66
67 /// <summary>
68 /// Print output from the tool execution to the console
69 /// </summary>
70 public bool PrintOutputToConsole { get; set; }
71
72 /// <summary>
73 /// The working directory of the tool
74 /// </summary>
75 public string WorkingDirectory { get; set; }
76
77 /// <summary>
78 /// Print the errors from the last run
79 /// </summary>
80 public void PrintErrors()
81 {
82 if (null != this.Errors)
83 {
84 Console.WriteLine("Errors:");
85
86 foreach (string error in this.Errors)
87 {
88 Console.WriteLine(error);
89 }
90 }
91 }
92
93 /// <summary>
94 /// Run the tool
95 /// </summary>
96 /// <returns>The results of the run</returns>
97 public ExternalExecutableResult Run()
98 {
99 return this.Run(true);
100 }
101
102 /// <summary>
103 /// Run the tool
104 /// </summary>
105 /// <param name="exceptionOnError">Throw an exception if the expected results don't match the actual results</param>
106 /// <exception cref="System.Exception">Thrown when the expected results don't match the actual results</exception>
107 /// <returns>The results of the run</returns>
108 public virtual ExternalExecutableResult Run(bool assertOnError)
109 {
110 var result = this.Run(this.Arguments, workingDirectory: this.WorkingDirectory ?? String.Empty);
111
112 if (this.PrintOutputToConsole)
113 {
114 Console.WriteLine(FormatResult(result));
115 }
116
117 this.Errors = this.CheckResult(result);
118
119 if (assertOnError && 0 < this.Errors.Count)
120 {
121 if (this.PrintOutputToConsole)
122 {
123 this.PrintErrors();
124 }
125
126 Assert.Empty(this.Errors);
127 }
128
129 return result;
130 }
131
132 /// <summary>
133 /// Checks that the result from a run matches the expected results
134 /// </summary>
135 /// <param name="result">A result from a run</param>
136 /// <returns>A list of errors</returns>
137 public virtual List<string> CheckResult(ExternalExecutableResult result)
138 {
139 List<string> errors = new List<string>();
140
141 // Verify that the expected return code matched the actual return code
142 if (null != this.ExpectedExitCode && this.ExpectedExitCode != result.ExitCode)
143 {
144 errors.Add(String.Format("Expected exit code {0} did not match actual exit code {1}", this.ExpectedExitCode, result.ExitCode));
145 }
146
147 var standardErrorString = string.Join(Environment.NewLine, result.StandardError);
148
149 // Verify that the expected error string are in stderr
150 if (null != this.ExpectedErrorStrings)
151 {
152 foreach (string expectedString in this.ExpectedErrorStrings)
153 {
154 if (!standardErrorString.Contains(expectedString))
155 {
156 errors.Add(String.Format("The text '{0}' was not found in stderr", expectedString));
157 }
158 }
159 }
160
161 var standardOutputString = string.Join(Environment.NewLine, result.StandardOutput);
162
163 // Verify that the expected output string are in stdout
164 if (null != this.ExpectedOutputStrings)
165 {
166 foreach (string expectedString in this.ExpectedOutputStrings)
167 {
168 if (!standardOutputString.Contains(expectedString))
169 {
170 errors.Add(String.Format("The text '{0}' was not found in stdout", expectedString));
171 }
172 }
173 }
174
175 // Verify that the expected regular expressions match stderr
176 if (null != this.ExpectedOutputRegexs)
177 {
178 foreach (Regex expectedRegex in this.ExpectedOutputRegexs)
179 {
180 if (!expectedRegex.IsMatch(standardOutputString))
181 {
182 errors.Add(String.Format("Regex {0} did not match stdout", expectedRegex.ToString()));
183 }
184 }
185 }
186
187 // Verify that the expected regular expressions match stdout
188 if (null != this.ExpectedErrorRegexs)
189 {
190 foreach (Regex expectedRegex in this.ExpectedErrorRegexs)
191 {
192 if (!expectedRegex.IsMatch(standardErrorString))
193 {
194 errors.Add(String.Format("Regex {0} did not match stderr", expectedRegex.ToString()));
195 }
196 }
197 }
198
199 return errors;
200 }
201
202 /// <summary>
203 /// Clears all of the expected results and resets them to the default values
204 /// </summary>
205 public virtual void SetDefaultExpectedResults()
206 {
207 this.ExpectedErrorRegexs = new List<Regex>();
208 this.ExpectedErrorStrings = new List<string>();
209 this.ExpectedExitCode = null;
210 this.ExpectedOutputRegexs = new List<Regex>();
211 this.ExpectedOutputStrings = new List<string>();
212 }
213
214 /// <summary>
215 /// Returns a string with data contained in the result.
216 /// </summary>
217 /// <returns>A string</returns>
218 private static string FormatResult(ExternalExecutableResult result)
219 {
220 var returnValue = new StringBuilder();
221 returnValue.AppendLine();
222 returnValue.AppendLine("----------------");
223 returnValue.AppendLine("Tool run result:");
224 returnValue.AppendLine("----------------");
225 returnValue.AppendLine("Command:");
226 returnValue.AppendLine($"\"{result.StartInfo.FileName}\" {result.StartInfo.Arguments}");
227 returnValue.AppendLine();
228 returnValue.AppendLine("Standard Output:");
229 foreach (var line in result.StandardOutput ?? new string[0])
230 {
231 returnValue.AppendLine(line);
232 }
233 returnValue.AppendLine("Standard Error:");
234 foreach (var line in result.StandardError ?? new string[0])
235 {
236 returnValue.AppendLine(line);
237 }
238 returnValue.AppendLine("Exit Code:");
239 returnValue.AppendLine(Convert.ToString(result.ExitCode));
240 returnValue.AppendLine("----------------");
241
242 return returnValue.ToString();
243 }
244 }
245}
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 @@
1// 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.
2
3namespace WixTestTools
4{
5 using Xunit.Abstractions;
6
7 public abstract class WixTestBase
8 {
9 protected WixTestBase(ITestOutputHelper testOutputHelper)
10 {
11 this.TestContext = new WixTestContext(testOutputHelper);
12 }
13
14 /// <summary>
15 /// The test context for the current test.
16 /// </summary>
17 public WixTestContext TestContext { get; }
18 }
19}
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 @@
1// 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.
2
3namespace WixTestTools
4{
5 using System;
6 using System.IO;
7 using System.Linq;
8 using System.Reflection;
9 using Microsoft.Win32;
10 using WixBuildTools.TestSupport;
11 using Xunit.Abstractions;
12
13 public class WixTestContext
14 {
15 static readonly string RootDataPath = Path.GetFullPath(TestData.Get("TestData"));
16
17 public WixTestContext(ITestOutputHelper testOutputHelper)
18 {
19 var test = GetTest(testOutputHelper);
20 var splitClassName = test.TestCase.TestMethod.TestClass.Class.Name.Split('.');
21
22 this.TestGroupName = splitClassName.Last();
23 this.TestName = test.TestCase.TestMethod.Method.Name;
24
25 this.TestDataFolder = Path.Combine(RootDataPath, this.TestGroupName);
26 }
27
28 public string TestDataFolder { get; }
29
30 /// <summary>
31 /// Gets the name of the current test group.
32 /// </summary>
33 public string TestGroupName { get; }
34
35 public string TestName { get; }
36
37 /// <summary>
38 /// Gets the test install directory for the current test.
39 /// </summary>
40 /// <param name="additionalPath">Additional subdirectories under the test install directory.</param>
41 /// <returns>Full path to the test install directory.</returns>
42 /// <remarks>
43 /// The package or bundle must install into [ProgramFilesFolder]\~Test WiX\[TestGroupName]\([Additional]).
44 /// </remarks>
45 public string GetTestInstallFolder(string additionalPath = null)
46 {
47 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "~Test WiX", this.TestGroupName, additionalPath ?? String.Empty);
48 }
49
50 /// <summary>
51 /// Gets the test registry key for the current test.
52 /// </summary>
53 /// <param name="additionalPath">Additional subkeys under the test registry key.</param>
54 /// <returns>Full path to the test registry key.</returns>
55 /// <remarks>
56 /// The package must write into HKLM\Software\WiX\Tests\[TestGroupName]\([Additional]).
57 /// </remarks>
58 public RegistryKey GetTestRegistryRoot(string additionalPath = null)
59 {
60 var key = String.Format(@"Software\WOW6432Node\WiX\Tests\{0}\{1}", this.TestGroupName, additionalPath ?? String.Empty);
61 return Registry.LocalMachine.OpenSubKey(key, true);
62 }
63
64 private static ITest GetTest(ITestOutputHelper output)
65 {
66 // https://github.com/xunit/xunit/issues/416#issuecomment-378512739
67 var type = output.GetType();
68 var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic);
69 var test = (ITest)testMember.GetValue(output);
70 return test;
71 }
72 }
73}
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 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- 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. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <TargetFramework>netcoreapp3.1</TargetFramework>
7 <PlatformTarget>x64</PlatformTarget>
8 </PropertyGroup>
9
10 <ItemGroup>
11 <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
12 <PackageReference Include="System.Security.Principal.Windows" Version="5.0.0" />
13 <PackageReference Include="WixBuildTools.TestSupport" Version="4.0.48" />
14 <PackageReference Include="WixToolset.Data" Version="4.0.185" />
15 <PackageReference Include="WixToolset.Mba.Core" Version="4.0.48" />
16 </ItemGroup>
17
18 <ItemGroup>
19 <PackageReference Include="xunit" Version="2.4.1" />
20 </ItemGroup>
21</Project>