aboutsummaryrefslogtreecommitdiff
path: root/src/test/burn/WixTestTools
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2021-04-22 17:12:34 -0700
committerRob Mensching <rob@firegiant.com>2021-05-05 11:18:35 -0700
commitd8e47230e094a506406a83eb78916abf2668b29c (patch)
tree2213ee3ed1a19fd5cd19a5914a23b7f7a57318ff /src/test/burn/WixTestTools
parent2cbe83832cc76aa379b29665de5523e82c543acf (diff)
downloadwix-d8e47230e094a506406a83eb78916abf2668b29c.tar.gz
wix-d8e47230e094a506406a83eb78916abf2668b29c.tar.bz2
wix-d8e47230e094a506406a83eb78916abf2668b29c.zip
Move Integration into test
Diffstat (limited to 'src/test/burn/WixTestTools')
-rw-r--r--src/test/burn/WixTestTools/BundleInstaller.cs197
-rw-r--r--src/test/burn/WixTestTools/BundleRegistration.cs182
-rw-r--r--src/test/burn/WixTestTools/BundleVerifier.cs156
-rw-r--r--src/test/burn/WixTestTools/LogVerifier.cs252
-rw-r--r--src/test/burn/WixTestTools/MSIExec.cs753
-rw-r--r--src/test/burn/WixTestTools/MsiUtilities.cs47
-rw-r--r--src/test/burn/WixTestTools/PackageInstaller.cs104
-rw-r--r--src/test/burn/WixTestTools/PackageVerifier.cs81
-rw-r--r--src/test/burn/WixTestTools/TestTool.cs245
-rw-r--r--src/test/burn/WixTestTools/WixTestBase.cs19
-rw-r--r--src/test/burn/WixTestTools/WixTestContext.cs75
-rw-r--r--src/test/burn/WixTestTools/WixTestTools.csproj21
12 files changed, 2132 insertions, 0 deletions
diff --git a/src/test/burn/WixTestTools/BundleInstaller.cs b/src/test/burn/WixTestTools/BundleInstaller.cs
new file mode 100644
index 00000000..a49c4024
--- /dev/null
+++ b/src/test/burn/WixTestTools/BundleInstaller.cs
@@ -0,0 +1,197 @@
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.TestContext = testContext;
16 this.TestGroupName = testContext.TestGroupName;
17 this.TestName = testContext.TestName;
18 }
19
20 public string Bundle { get; }
21
22 private WixTestContext TestContext { get; }
23
24 public string TestGroupName { get; }
25
26 public string TestName { get; }
27
28 /// <summary>
29 /// Installs the bundle with optional arguments.
30 /// </summary>
31 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
32 /// <param name="arguments">Optional arguments to pass to the tool.</param>
33 /// <returns>Path to the generated log file.</returns>
34 public string Install(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
35 {
36 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Install, arguments);
37 }
38
39 /// <summary>
40 /// Installs the bundle with optional arguments.
41 /// </summary>
42 /// <param name="bundlePath">This should be the bundle in the package cache.</param>
43 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
44 /// <param name="arguments">Optional arguments to pass to the tool.</param>
45 /// <returns>Path to the generated log file.</returns>
46 public string Install(string bundlePath, int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
47 {
48 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Install, arguments, bundlePath: bundlePath);
49 }
50
51 /// <summary>
52 /// Calls Layout for the bundle with optional arguments.
53 /// </summary>
54 /// <param name="layoutDirectory">The destination directory.</param>
55 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
56 /// <param name="arguments">Optional arguments to pass to the tool.</param>
57 /// <returns>Path to the generated log file.</returns>
58 public string Layout(string layoutDirectory, int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
59 {
60 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.AdministrativeInstall, arguments, layoutDirectory: layoutDirectory);
61 }
62
63 /// <summary>
64 /// Calls Layout for the bundle with optional arguments.
65 /// </summary>
66 /// <param name="bundlePath">Path to the bundle to run.</param>
67 /// <param name="layoutDirectory">The destination directory.</param>
68 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
69 /// <param name="arguments">Optional arguments to pass to the tool.</param>
70 /// <returns>Path to the generated log file.</returns>
71 public string Layout(string bundlePath, string layoutDirectory, int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
72 {
73 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.AdministrativeInstall, arguments, bundlePath: bundlePath, layoutDirectory: layoutDirectory);
74 }
75
76 /// <summary>
77 /// Modify the bundle with optional arguments.
78 /// </summary>
79 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
80 /// <param name="arguments">Optional arguments to pass to the tool.</param>
81 /// <returns>Path to the generated log file.</returns>
82 public string Modify(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
83 {
84 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Modify, arguments);
85 }
86
87 /// <summary>
88 /// Modify the bundle with optional arguments.
89 /// </summary>
90 /// <param name="bundlePath">This should be the bundle in the package cache.</param>
91 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
92 /// <param name="arguments">Optional arguments to pass to the tool.</param>
93 /// <returns>Path to the generated log file.</returns>
94 public string Modify(string bundlePath, int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
95 {
96 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Modify, arguments, bundlePath: bundlePath);
97 }
98
99 /// <summary>
100 /// Repairs the bundle with optional arguments.
101 /// </summary>
102 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
103 /// <param name="arguments">Optional arguments to pass to the tool.</param>
104 /// <returns>Path to the generated log file.</returns>
105 public string Repair(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
106 {
107 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Repair, arguments);
108 }
109
110 /// <summary>
111 /// Uninstalls the bundle with optional arguments.
112 /// </summary>
113 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
114 /// <param name="arguments">Optional arguments to pass to the tool.</param>
115 /// <returns>Path to the generated log file.</returns>
116 public string Uninstall(int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
117 {
118 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Uninstall, arguments);
119 }
120
121 /// <summary>
122 /// Uninstalls the bundle at the given path with optional arguments.
123 /// </summary>
124 /// <param name="bundlePath">This should be the bundle in the package cache.</param>
125 /// <param name="expectedExitCode">Expected exit code, defaults to success.</param>
126 /// <param name="arguments">Optional arguments to pass to the tool.</param>
127 /// <returns>Path to the generated log file.</returns>
128 public string Uninstall(string bundlePath, int expectedExitCode = (int)MSIExec.MSIExecReturnCode.SUCCESS, params string[] arguments)
129 {
130 return this.RunBundleWithArguments(expectedExitCode, MSIExec.MSIExecMode.Uninstall, arguments, bundlePath: bundlePath);
131 }
132
133 /// <summary>
134 /// Executes the bundle with optional arguments.
135 /// </summary>
136 /// <param name="expectedExitCode">Expected exit code.</param>
137 /// <param name="mode">Install mode.</param>
138 /// <param name="arguments">Optional arguments to pass to the tool.</param>
139 /// <returns>Path to the generated log file.</returns>
140 private string RunBundleWithArguments(int expectedExitCode, MSIExec.MSIExecMode mode, string[] arguments, bool assertOnError = true, string bundlePath = null, string layoutDirectory = null)
141 {
142 TestTool bundle = new TestTool(bundlePath ?? this.Bundle);
143 var sb = new StringBuilder();
144
145 // Be sure to run silent.
146 sb.Append(" -quiet");
147
148 // Generate the log file name.
149 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));
150 sb.AppendFormat(" -log \"{0}\"", logFile);
151
152 // Set operation.
153 switch (mode)
154 {
155 case MSIExec.MSIExecMode.AdministrativeInstall:
156 sb.Append($" -layout \"{layoutDirectory}\"");
157 break;
158
159 case MSIExec.MSIExecMode.Modify:
160 sb.Append(" -modify");
161 break;
162
163 case MSIExec.MSIExecMode.Repair:
164 sb.Append(" -repair");
165 break;
166
167 case MSIExec.MSIExecMode.Cleanup:
168 case MSIExec.MSIExecMode.Uninstall:
169 sb.Append(" -uninstall");
170 break;
171 }
172
173 // Add additional arguments.
174 if (null != arguments)
175 {
176 sb.Append(" ");
177 sb.Append(String.Join(" ", arguments));
178 }
179
180 // Set the arguments.
181 bundle.Arguments = sb.ToString();
182
183 // Run the tool and assert the expected code.
184 bundle.ExpectedExitCode = expectedExitCode;
185 bundle.Run(assertOnError);
186
187 // Return the log file name.
188 return logFile;
189 }
190
191 public void Dispose()
192 {
193 string[] args = { "-burn.ignoredependencies=ALL" };
194 this.RunBundleWithArguments((int)MSIExec.MSIExecReturnCode.SUCCESS, MSIExec.MSIExecMode.Cleanup, args, assertOnError: false);
195 }
196 }
197}
diff --git a/src/test/burn/WixTestTools/BundleRegistration.cs b/src/test/burn/WixTestTools/BundleRegistration.cs
new file mode 100644
index 00000000..75660838
--- /dev/null
+++ b/src/test/burn/WixTestTools/BundleRegistration.cs
@@ -0,0 +1,182 @@
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\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
11 public const string BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY_WOW6432NODE = "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
12 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH = "BundleCachePath";
13 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_ADDON_CODE = "BundleAddonCode";
14 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_DETECT_CODE = "BundleDetectCode";
15 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_PATCH_CODE = "BundlePatchCode";
16 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE = "BundleUpgradeCode";
17 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_DISPLAY_NAME = "DisplayName";
18 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION = "BundleVersion";
19 public const string BURN_REGISTRATION_REGISTRY_ENGINE_VERSION = "EngineVersion";
20 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_PROVIDER_KEY = "BundleProviderKey";
21 public const string BURN_REGISTRATION_REGISTRY_BUNDLE_TAG = "BundleTag";
22 public const string REGISTRY_REBOOT_PENDING_FORMAT = "{0}.RebootRequired";
23 public const string REGISTRY_BUNDLE_INSTALLED = "Installed";
24 public const string REGISTRY_BUNDLE_DISPLAY_ICON = "DisplayIcon";
25 public const string REGISTRY_BUNDLE_DISPLAY_VERSION = "DisplayVersion";
26 public const string REGISTRY_BUNDLE_ESTIMATED_SIZE = "EstimatedSize";
27 public const string REGISTRY_BUNDLE_PUBLISHER = "Publisher";
28 public const string REGISTRY_BUNDLE_HELP_LINK = "HelpLink";
29 public const string REGISTRY_BUNDLE_HELP_TELEPHONE = "HelpTelephone";
30 public const string REGISTRY_BUNDLE_URL_INFO_ABOUT = "URLInfoAbout";
31 public const string REGISTRY_BUNDLE_URL_UPDATE_INFO = "URLUpdateInfo";
32 public const string REGISTRY_BUNDLE_PARENT_DISPLAY_NAME = "ParentDisplayName";
33 public const string REGISTRY_BUNDLE_PARENT_KEY_NAME = "ParentKeyName";
34 public const string REGISTRY_BUNDLE_COMMENTS = "Comments";
35 public const string REGISTRY_BUNDLE_CONTACT = "Contact";
36 public const string REGISTRY_BUNDLE_NO_MODIFY = "NoModify";
37 public const string REGISTRY_BUNDLE_MODIFY_PATH = "ModifyPath";
38 public const string REGISTRY_BUNDLE_NO_ELEVATE_ON_MODIFY = "NoElevateOnModify";
39 public const string REGISTRY_BUNDLE_NO_REMOVE = "NoRemove";
40 public const string REGISTRY_BUNDLE_SYSTEM_COMPONENT = "SystemComponent";
41 public const string REGISTRY_BUNDLE_QUIET_UNINSTALL_STRING = "QuietUninstallString";
42 public const string REGISTRY_BUNDLE_UNINSTALL_STRING = "UninstallString";
43 public const string REGISTRY_BUNDLE_RESUME_COMMAND_LINE = "BundleResumeCommandLine";
44 public const string REGISTRY_BUNDLE_VERSION_MAJOR = "VersionMajor";
45 public const string REGISTRY_BUNDLE_VERSION_MINOR = "VersionMinor";
46
47 public string[] AddonCodes { get; set; }
48
49 public string CachePath { get; set; }
50
51 public string DisplayName { get; set; }
52
53 public string[] DetectCodes { get; set; }
54
55 public string EngineVersion { get; set; }
56
57 public int? EstimatedSize { get; set; }
58
59 public int? Installed { get; set; }
60
61 public string ModifyPath { get; set; }
62
63 public string[] PatchCodes { get; set; }
64
65 public string ProviderKey { get; set; }
66
67 public string Publisher { get; set; }
68
69 public string QuietUninstallString { get; set; }
70
71 public string QuietUninstallCommand { get; set; }
72
73 public string QuietUninstallCommandArguments { get; set; }
74
75 public string Tag { get; set; }
76
77 public string UninstallCommand { get; set; }
78
79 public string UninstallCommandArguments { get; set; }
80
81 public string UninstallString { get; set; }
82
83 public string[] UpgradeCodes { get; set; }
84
85 public string UrlInfoAbout { get; set; }
86
87 public string UrlUpdateInfo { get; set; }
88
89 public string Version { get; set; }
90
91 public static bool TryGetPerMachineBundleRegistrationById(string bundleId, bool x64, out BundleRegistration registration)
92 {
93 var baseKeyPath = x64 ? BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY : BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY_WOW6432NODE;
94 var registrationKeyPath = $"{baseKeyPath}\\{bundleId}";
95 using var registrationKey = Registry.LocalMachine.OpenSubKey(registrationKeyPath);
96 var success = registrationKey != null;
97 registration = success ? GetBundleRegistration(registrationKey) : null;
98 return success;
99 }
100
101 public static bool TryGetPerUserBundleRegistrationById(string bundleId, out BundleRegistration registration)
102 {
103 var registrationKeyPath = $"{BURN_REGISTRATION_REGISTRY_UNINSTALL_KEY}\\{bundleId}";
104 using var registrationKey = Registry.CurrentUser.OpenSubKey(registrationKeyPath);
105 var success = registrationKey != null;
106 registration = success ? GetBundleRegistration(registrationKey) : null;
107 return success;
108 }
109
110 private static BundleRegistration GetBundleRegistration(RegistryKey idKey)
111 {
112 var registration = new BundleRegistration();
113
114 registration.AddonCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_ADDON_CODE) as string[];
115 registration.CachePath = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_CACHE_PATH) as string;
116 registration.DetectCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_DETECT_CODE) as string[];
117 registration.PatchCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_PATCH_CODE) as string[];
118 registration.ProviderKey = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_PROVIDER_KEY) as string;
119 registration.Tag = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_TAG) as string;
120 registration.UpgradeCodes = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_UPGRADE_CODE) as string[];
121 registration.Version = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_VERSION) as string;
122 registration.DisplayName = idKey.GetValue(BURN_REGISTRATION_REGISTRY_BUNDLE_DISPLAY_NAME) as string;
123 registration.EngineVersion = idKey.GetValue(BURN_REGISTRATION_REGISTRY_ENGINE_VERSION) as string;
124 registration.EstimatedSize = idKey.GetValue(REGISTRY_BUNDLE_ESTIMATED_SIZE) as int?;
125 registration.Installed = idKey.GetValue(REGISTRY_BUNDLE_INSTALLED) as int?;
126 registration.ModifyPath = idKey.GetValue(REGISTRY_BUNDLE_MODIFY_PATH) as string;
127 registration.Publisher = idKey.GetValue(REGISTRY_BUNDLE_PUBLISHER) as string;
128 registration.UrlInfoAbout = idKey.GetValue(REGISTRY_BUNDLE_URL_INFO_ABOUT) as string;
129 registration.UrlUpdateInfo = idKey.GetValue(REGISTRY_BUNDLE_URL_UPDATE_INFO) as string;
130
131 registration.QuietUninstallString = idKey.GetValue(REGISTRY_BUNDLE_QUIET_UNINSTALL_STRING) as string;
132 if (!String.IsNullOrEmpty(registration.QuietUninstallString))
133 {
134 var closeQuote = registration.QuietUninstallString.IndexOf("\"", 1);
135 if (closeQuote > 0)
136 {
137 registration.QuietUninstallCommand = registration.QuietUninstallString.Substring(1, closeQuote - 1).Trim();
138 registration.QuietUninstallCommandArguments = registration.QuietUninstallString.Substring(closeQuote + 1).Trim();
139 }
140 }
141
142 registration.UninstallString = idKey.GetValue(REGISTRY_BUNDLE_UNINSTALL_STRING) as string;
143 if (!String.IsNullOrEmpty(registration.UninstallString))
144 {
145 var closeQuote = registration.UninstallString.IndexOf("\"", 1);
146 if (closeQuote > 0)
147 {
148 registration.UninstallCommand = registration.UninstallString.Substring(1, closeQuote - 1).Trim();
149 registration.UninstallCommandArguments = registration.UninstallString.Substring(closeQuote + 1).Trim();
150 }
151 }
152
153 return registration;
154 }
155
156 public static bool TryGetDependencyProviderValue(string providerId, string name, out string value)
157 {
158 value = null;
159
160 string key = String.Format(@"Installer\Dependencies\{0}", providerId);
161 using (RegistryKey providerKey = Registry.ClassesRoot.OpenSubKey(key))
162 {
163 if (null == providerKey)
164 {
165 return false;
166 }
167
168 value = providerKey.GetValue(name) as string;
169 return value != null;
170 }
171 }
172
173 public static bool DependencyDependentExists(string providerId, string dependentId)
174 {
175 string key = String.Format(@"Installer\Dependencies\{0}\Dependents\{1}", providerId, dependentId);
176 using (RegistryKey dependentKey = Registry.ClassesRoot.OpenSubKey(key))
177 {
178 return null != dependentKey;
179 }
180 }
181 }
182}
diff --git a/src/test/burn/WixTestTools/BundleVerifier.cs b/src/test/burn/WixTestTools/BundleVerifier.cs
new file mode 100644
index 00000000..984df169
--- /dev/null
+++ b/src/test/burn/WixTestTools/BundleVerifier.cs
@@ -0,0 +1,156 @@
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, bool perMachine)
37 {
38 string cachePath;
39 if (perMachine)
40 {
41 using var policyKey = Registry.LocalMachine.OpenSubKey(FULL_BURN_POLICY_REGISTRY_PATH);
42 var redirectedCachePath = policyKey?.GetValue("PackageCache") as string;
43 cachePath = redirectedCachePath ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), PACKAGE_CACHE_FOLDER_NAME);
44 }
45 else
46 {
47 cachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), PACKAGE_CACHE_FOLDER_NAME);
48 }
49 return Path.Combine(cachePath, cacheId);
50 }
51
52 public string GetExpectedCachedBundlePath()
53 {
54 var bundleSymbol = this.GetBundleSymbol();
55 var cachePath = this.GetPackageCachePathForCacheId(bundleSymbol.BundleId, bundleSymbol.PerMachine);
56 return Path.Combine(cachePath, Path.GetFileName(this.Bundle));
57 }
58
59 public string ManuallyCache()
60 {
61 var expectedCachePath = this.GetExpectedCachedBundlePath();
62 Directory.CreateDirectory(Path.GetDirectoryName(expectedCachePath));
63 File.Copy(this.Bundle, expectedCachePath);
64 return expectedCachePath;
65 }
66
67 public void ManuallyUncache()
68 {
69 var expectedCachePath = this.GetExpectedCachedBundlePath();
70 File.Delete(expectedCachePath);
71 }
72
73 public bool TryGetRegistration(out BundleRegistration registration)
74 {
75 var bundleSymbol = this.GetBundleSymbol();
76 var x64 = bundleSymbol.Platform != Platform.X86;
77 var bundleId = bundleSymbol.BundleId;
78 if (bundleSymbol.PerMachine)
79 {
80 return BundleRegistration.TryGetPerMachineBundleRegistrationById(bundleId, x64, out registration);
81 }
82 else
83 {
84 return BundleRegistration.TryGetPerUserBundleRegistrationById(bundleId, out registration);
85 }
86 }
87
88 public string VerifyRegisteredAndInPackageCache()
89 {
90 Assert.True(this.TryGetRegistration(out var registration));
91
92 Assert.NotNull(registration.CachePath);
93 Assert.True(File.Exists(registration.CachePath));
94
95 var expectedCachePath = this.GetExpectedCachedBundlePath();
96 Assert.Equal(expectedCachePath, registration.CachePath, StringComparer.OrdinalIgnoreCase);
97
98 return registration.CachePath;
99 }
100
101 public void VerifyUnregisteredAndRemovedFromPackageCache()
102 {
103 var cachedBundlePath = this.GetExpectedCachedBundlePath();
104 this.VerifyUnregisteredAndRemovedFromPackageCache(cachedBundlePath);
105 }
106
107 public void VerifyUnregisteredAndRemovedFromPackageCache(string cachedBundlePath)
108 {
109 Assert.False(this.TryGetRegistration(out _));
110 Assert.False(File.Exists(cachedBundlePath));
111 }
112
113 public void RemovePackageFromCache(string packageId)
114 {
115 using var wixOutput = WixOutput.Read(this.BundlePdb);
116 var intermediate = Intermediate.Load(wixOutput);
117 var section = intermediate.Sections.Single();
118 var packageSymbol = section.Symbols.OfType<WixBundlePackageSymbol>().Single(p => p.Id.Id == packageId);
119 var cachePath = this.GetPackageCachePathForCacheId(packageSymbol.CacheId, packageSymbol.PerMachine == YesNoDefaultType.Yes);
120 if (Directory.Exists(cachePath))
121 {
122 Directory.Delete(cachePath, true);
123 }
124 }
125
126 public void VerifyPackageIsCached(string packageId)
127 {
128 using var wixOutput = WixOutput.Read(this.BundlePdb);
129 var intermediate = Intermediate.Load(wixOutput);
130 var section = intermediate.Sections.Single();
131 var packageSymbol = section.Symbols.OfType<WixBundlePackageSymbol>().Single(p => p.Id.Id == packageId);
132 var cachePath = this.GetPackageCachePathForCacheId(packageSymbol.CacheId, packageSymbol.PerMachine == YesNoDefaultType.Yes);
133 Assert.True(Directory.Exists(cachePath));
134 }
135
136 public void VerifyExeTestRegistryRootDeleted(string name, bool x64 = false)
137 {
138 using var testRegistryRoot = this.TestContext.GetTestRegistryRoot(x64, name);
139 if (testRegistryRoot != null)
140 {
141 var actualValue = testRegistryRoot.GetValue("Version") as string;
142 Assert.Null(actualValue);
143 }
144 }
145
146 public void VerifyExeTestRegistryValue(string name, string expectedValue, bool x64 = false)
147 {
148 using (var root = this.TestContext.GetTestRegistryRoot(x64, name))
149 {
150 Assert.NotNull(root);
151 var actualValue = root.GetValue("Version") as string;
152 Assert.Equal(expectedValue, actualValue);
153 }
154 }
155 }
156}
diff --git a/src/test/burn/WixTestTools/LogVerifier.cs b/src/test/burn/WixTestTools/LogVerifier.cs
new file mode 100644
index 00000000..0252a9f9
--- /dev/null
+++ b/src/test/burn/WixTestTools/LogVerifier.cs
@@ -0,0 +1,252 @@
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 System.Text.RegularExpressions;
9 using Xunit;
10
11 /// <summary>
12 /// The LogVerifier can verify a log file for given regular expressions.
13 /// </summary>
14 public class LogVerifier
15 {
16 // Member Variables
17 private string logFile;
18
19 /// <summary>
20 /// Prevent creation of LogVerifier without log file
21 /// </summary>
22 private LogVerifier()
23 { }
24
25 /// <summary>
26 /// Constructor for log files where the exact file name is known.
27 /// </summary>
28 /// <param name="fileName">The full path to the log file</param>
29 public LogVerifier(string fileName)
30 {
31 if (null == fileName)
32 throw new ArgumentNullException("fileName");
33
34 if (!File.Exists(fileName))
35 throw new ArgumentException(String.Format(@"File doesn't exist:{0}", fileName), "fileName");
36
37 logFile = fileName;
38 }
39
40 /// <summary>
41 /// Constructor for log files where the exact file name is known.
42 /// </summary>
43 /// <param name="directory">The directory in which the log file is located.</param>
44 /// <param name="fileName">The name of the log file.</param>
45 public LogVerifier(string directory, string fileName)
46 : this(Path.Combine(directory, fileName))
47 { }
48
49 /// <summary>
50 /// Scans a log file line by line until the regex pattern is matched or eof is reached.
51 /// This method would be used in the case where the log file is very large, the regex doesn't
52 /// span multiple lines, and only one match is required.
53 /// </summary>
54 /// <param name="regex">A regular expression</param>
55 /// <returns>True if a match is found, False otherwise.</returns>
56 public bool LineByLine(Regex regex)
57 {
58 string line = string.Empty;
59 StreamReader sr = new StreamReader(logFile);
60
61 // Read from a file stream line by line.
62 while ((line = sr.ReadLine()) != null)
63 {
64 if (regex.Match(line).Success)
65 {
66 sr.Close();
67 sr.Dispose();
68 return true;
69 }
70 }
71 return false;
72 }
73
74
75 /// <summary>
76 /// Scans a log file line by line until the regex pattern is matched or eof is reached.
77 /// This method would be used in the case where the log file is very large, the regex doesn't
78 /// span multiple lines, and only one match is required.
79 /// No RegexOptions are used and matches are case sensitive.
80 /// </summary>
81 /// <param name="regex">A regular expression string.</param>
82 /// <returns>True if a match is found, False otherwise.</returns>
83 public bool LineByLine(string regex)
84 {
85 return LineByLine(new Regex(regex));
86 }
87
88
89 /// <summary>
90 /// Scans a log file for matches to the regex.
91 /// </summary>
92 /// <param name="regex">A regular expression</param>
93 /// <returns>The number of matches</returns>
94 public int EntireFileAtOnce(Regex regex)
95 {
96 string logFileText = this.ReadLogFile();
97 return regex.Matches(logFileText).Count;
98 }
99
100 /// <summary>
101 /// Scans a log file for matches to the regex.
102 /// </summary>
103 /// <param name="regex">A regular expression</param>
104 /// <returns>The number of matches</returns>
105 public bool EntireFileAtOncestr(string regex)
106 {
107 string logFileText = this.ReadLogFile();
108 return logFileText.Contains(regex);
109 }
110 /// <summary>
111 /// Scans a log file for matches to the regex string.
112 /// Only the Multiline RegexOption is used and matches are case sensitive.
113 /// </summary>
114 /// <param name="regex">A regular expression</param>
115 /// <returns>The number of matches</returns>
116 public int EntireFileAtOnce(string regex)
117 {
118 return EntireFileAtOnce(new Regex(regex, RegexOptions.Multiline));
119 }
120
121 /// <summary>
122 /// Scans a log file for matches to the regex string.
123 /// </summary>
124 /// <param name="regex">A regular expression</param>
125 /// <param name="ignoreCase">Specify whether to perform case sensitive matches</param>
126 /// <returns>The number of matches</returns>
127 public int EntireFileAtOnce(string regex, bool ignoreCase)
128 {
129 if (!ignoreCase)
130 return EntireFileAtOnce(new Regex(regex, RegexOptions.Multiline));
131 else
132 return EntireFileAtOnce(new Regex(regex, RegexOptions.Multiline | RegexOptions.IgnoreCase));
133 }
134
135 /// <summary>
136 /// Search through the log and Assert.Fail() if a specified string is not found.
137 /// </summary>
138 /// <param name="regex">Search expression</param>
139 /// <param name="ignoreCase">Perform case insensitive match</param>
140 public void AssertTextInLog(string regex, bool ignoreCase)
141 {
142 Assert.True(EntireFileAtOncestr(regex),
143 String.Format("The log does not contain a match to the regular expression \"{0}\" ", regex));
144 }
145
146 /// <summary>
147 /// Search through the log and Assert.Fail() if a specified string is not found.
148 /// </summary>
149 /// <param name="regex">Search expression</param>
150 /// <param name="ignoreCase">Perform case insensitive match</param>
151 public void AssertTextInLog(Regex regex, bool ignoreCase)
152 {
153 Assert.True(EntireFileAtOnce(regex) >= 1,
154 String.Format("The log does not contain a match to the regular expression \"{0}\" ", regex.ToString()));
155 }
156
157 /// <summary>
158 /// Search through the log and Assert.Fail() if a specified string is not found.
159 /// </summary>
160 /// <param name="regex">Search expression</param>
161 /// <param name="ignoreCase">Perform case insensitive match</param>
162 public void AssertTextInLog(string regex)
163 {
164 AssertTextInLog(regex, true);
165 }
166
167 /// <summary>
168 /// Search through the log and Assert.Fail() if a specified string is not found.
169 /// </summary>
170 /// <param name="regex">Search expression</param>
171 /// <param name="ignoreCase">Perform case insensitive match</param>
172 public void AssertTextInLog(Regex regex)
173 {
174 AssertTextInLog(regex, true);
175 }
176
177
178 /// <summary>
179 /// Search through the log and Assert.Fail() if a specified string is found.
180 /// </summary>
181 /// <param name="regex">Search expression</param>
182 /// <param name="ignoreCase">Perform case insensitive match</param>
183 public void AssertTextNotInLog(Regex regex, bool ignoreCase)
184 {
185 Assert.True(EntireFileAtOnce(regex) < 1,
186 String.Format("The log contain a match to the regular expression \"{0}\" ", regex.ToString()));
187 }
188
189 /// <summary>
190 /// Search through the log and Assert.Fail() if a specified string is not found.
191 /// </summary>
192 /// <param name="regex">Search expression</param>
193 /// <param name="ignoreCase">Perform case insensitive match</param>
194 public void AssertTextNotInLog(string regex, bool ignoreCase)
195 {
196 Assert.False(EntireFileAtOncestr(regex),
197 String.Format("The log does not contain a match to the regular expression \"{0}\" ", regex));
198 }
199
200 /// <summary>
201 /// Checks if a meesage is in a file
202 /// </summary>
203 /// <param name="logFileName">The full path to the log file</param>
204 /// <param name="message">Search expression</param>
205 /// <returns>True if the message was found, false otherwise</returns>
206 public static bool MessageInLogFile(string logFileName, string message)
207 {
208 LogVerifier logVerifier = new LogVerifier(logFileName);
209 return logVerifier.EntireFileAtOncestr(message);
210 }
211
212 /// <summary>
213 /// Checks if a meesage is in a file
214 /// </summary>
215 /// <param name="logFileName">The full path to the log file</param>
216 /// <param name="message">Search expression (regex)</param>
217 /// <returns>True if the message was found, false otherwise</returns>
218 public static bool MessageInLogFileRegex(string logFileName, string regexMessage)
219 {
220 LogVerifier logVerifier = new LogVerifier(logFileName);
221 return logVerifier.EntireFileAtOnce(regexMessage) > 0;
222 }
223
224 /// <summary>
225 /// Read in the entire log file at once.
226 /// </summary>
227 /// <returns>Contents of log file.</returns>
228 private string ReadLogFile()
229 {
230 // Retry a few times.
231 for (int retry = 0; ; ++retry)
232 {
233 try
234 {
235 using (StreamReader sr = new StreamReader(this.logFile))
236 {
237 return sr.ReadToEnd();
238 }
239 }
240 catch // we'll catch everything a few times until we give up.
241 {
242 if (retry > 4)
243 {
244 throw;
245 }
246
247 System.Threading.Thread.Sleep(1000);
248 }
249 }
250 }
251 }
252}
diff --git a/src/test/burn/WixTestTools/MSIExec.cs b/src/test/burn/WixTestTools/MSIExec.cs
new file mode 100644
index 00000000..8dce96cf
--- /dev/null
+++ b/src/test/burn/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/test/burn/WixTestTools/MsiUtilities.cs b/src/test/burn/WixTestTools/MsiUtilities.cs
new file mode 100644
index 00000000..4c7d1601
--- /dev/null
+++ b/src/test/burn/WixTestTools/MsiUtilities.cs
@@ -0,0 +1,47 @@
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 WixToolset.Dtf.WindowsInstaller;
7
8 public class MsiUtilities
9 {
10 /// <summary>
11 /// Return true if it finds the given productcode in system otherwise it returns false
12 /// </summary>
13 /// <param name="prodCode"></param>
14 /// <returns></returns>
15 public static bool IsProductInstalled(string prodCode)
16 {
17 //look in all user's products (both per-machine and per-user)
18 foreach (ProductInstallation product in ProductInstallation.GetProducts(null, "s-1-1-0", UserContexts.All))
19 {
20 if (product.ProductCode == prodCode)
21 {
22 return true;
23 }
24 }
25 return false;
26 }
27
28 /// <summary>
29 /// Return true if it finds the given productcode in system with the specified version otherwise it returns false
30 /// </summary>
31 /// <param name="prodCode"></param>
32 /// <param name="prodVersion"></param>
33 /// <returns></returns>
34 public static bool IsProductInstalledWithVersion(string prodCode, Version prodVersion)
35 {
36 //look in all user's products (both per-machine and per-user)
37 foreach (ProductInstallation product in ProductInstallation.GetProducts(null, "s-1-1-0", UserContexts.All))
38 {
39 if (product.ProductCode == prodCode && product.ProductVersion == prodVersion)
40 {
41 return true;
42 }
43 }
44 return false;
45 }
46 }
47}
diff --git a/src/test/burn/WixTestTools/PackageInstaller.cs b/src/test/burn/WixTestTools/PackageInstaller.cs
new file mode 100644
index 00000000..d32f499b
--- /dev/null
+++ b/src/test/burn/WixTestTools/PackageInstaller.cs
@@ -0,0 +1,104 @@
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.Symbols;
10 using WixToolset.Data.WindowsInstaller;
11 using static WixTestTools.MSIExec;
12
13 public partial class PackageInstaller : IDisposable
14 {
15 public PackageInstaller(WixTestContext testContext, string filename)
16 {
17 this.Package = Path.Combine(testContext.TestDataFolder, $"{filename}.msi");
18 this.PackagePdb = Path.Combine(testContext.TestDataFolder, $"{filename}.wixpdb");
19 this.TestContext = testContext;
20
21 using var wixOutput = WixOutput.Read(this.PackagePdb);
22
23 var intermediate = Intermediate.Load(wixOutput);
24 var section = intermediate.Sections.Single();
25 var platformSummary = section.Symbols.OfType<SummaryInformationSymbol>().Single(s => s.PropertyId == SummaryInformationType.PlatformAndLanguage);
26 var platformString = platformSummary.Value.Split(new char[] { ';' }, 2)[0];
27 this.IsX64 = platformString != "Intel";
28
29 this.WiData = WindowsInstallerData.Load(wixOutput);
30 }
31
32 public string Package { get; }
33
34 private WixTestContext TestContext { get; }
35
36 public string TestGroupName => this.TestContext.TestGroupName;
37
38 public string TestName => this.TestContext.TestName;
39
40 /// <summary>
41 /// Installs a .msi file
42 /// </summary>
43 /// <param name="expectedExitCode">Expected exit code</param>
44 /// <param name="otherArguments">Other arguments to pass to MSIExec.</param>
45 /// <returns>MSIExec log File</returns>
46 public string InstallProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments)
47 {
48 return this.RunMSIExec(MSIExecMode.Install, otherArguments, expectedExitCode);
49 }
50
51 /// <summary>
52 /// Uninstalls a .msi file
53 /// </summary>
54 /// <param name="expectedExitCode">Expected exit code</param>
55 /// <param name="otherArguments">Other arguments to pass to MSIExec.</param>
56 /// <returns>MSIExec log File</returns>
57 public string UninstallProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments)
58 {
59 return this.RunMSIExec(MSIExecMode.Uninstall, otherArguments, expectedExitCode);
60 }
61
62 /// <summary>
63 /// Repairs a .msi file
64 /// </summary>
65 /// <param name="expectedExitCode">Expected exit code</param>
66 /// <param name="otherArguments">Other arguments to pass to msiexe.exe.</param>
67 /// <returns>MSIExec log File</returns>
68 public string RepairProduct(MSIExecReturnCode expectedExitCode = MSIExecReturnCode.SUCCESS, params string[] otherArguments)
69 {
70 return this.RunMSIExec(MSIExecMode.Repair, otherArguments, expectedExitCode);
71 }
72
73 /// <summary>
74 /// Executes MSIExec on a .msi file
75 /// </summary>
76 /// <param name="mode">Mode of execution for MSIExec</param>
77 /// <param name="otherArguments">Other arguments to pass to MSIExec.</param>
78 /// <param name="expectedExitCode">Expected exit code</param>
79 /// <returns>MSIExec exit code</returns>
80 private string RunMSIExec(MSIExecMode mode, string[] otherArguments, MSIExecReturnCode expectedExitCode, bool assertOnError = true)
81 {
82 // Generate the log file name.
83 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));
84
85 var msiexec = new MSIExec
86 {
87 Product = this.Package,
88 ExecutionMode = mode,
89 OtherArguments = null != otherArguments ? String.Join(" ", otherArguments) : null,
90 ExpectedExitCode = expectedExitCode,
91 LogFile = logFile,
92 };
93
94 msiexec.Run(assertOnError);
95 return msiexec.LogFile;
96 }
97
98 public void Dispose()
99 {
100 string[] args = { "IGNOREDEPENDENCIES=ALL", "WIXFAILWHENDEFERRED=0" };
101 this.RunMSIExec(MSIExecMode.Cleanup, args, MSIExecReturnCode.SUCCESS, assertOnError: false);
102 }
103 }
104}
diff --git a/src/test/burn/WixTestTools/PackageVerifier.cs b/src/test/burn/WixTestTools/PackageVerifier.cs
new file mode 100644
index 00000000..2f42dd21
--- /dev/null
+++ b/src/test/burn/WixTestTools/PackageVerifier.cs
@@ -0,0 +1,81 @@
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.WindowsInstaller;
9 using WixToolset.Data.WindowsInstaller.Rows;
10 using Xunit;
11
12 public partial class PackageInstaller
13 {
14 public string PackagePdb { get; }
15
16 private bool IsX64 { get; }
17
18 private WindowsInstallerData WiData { get; }
19
20 public string GetInstalledFilePath(string filename)
21 {
22 return this.TestContext.GetTestInstallFolder(this.IsX64, Path.Combine(this.GetInstallFolderName(), filename));
23 }
24
25 public string GetInstallFolderName()
26 {
27 var row = this.WiData.Tables["Directory"].Rows.Single(r => r.FieldAsString(0) == "INSTALLFOLDER");
28 var value = row.FieldAsString(2);
29 var longNameIndex = value.IndexOf('|') + 1;
30 if (longNameIndex > 0)
31 {
32 return value.Substring(longNameIndex);
33 }
34 return value;
35 }
36
37 public string GetProperty(string name)
38 {
39 var row = this.WiData.Tables["Property"].Rows.Cast<PropertyRow>().Single(r => r.Property == name);
40 return row.Value;
41 }
42
43 public void VerifyInstalled(bool installed)
44 {
45 var productCode = this.GetProperty("ProductCode");
46 Assert.Equal(installed, MsiUtilities.IsProductInstalled(productCode));
47 }
48
49 public void VerifyInstalledWithVersion(bool installed)
50 {
51 var productCode = this.GetProperty("ProductCode");
52 Version prodVersion = new Version(this.GetProperty("ProductVersion"));
53 Assert.Equal(installed, MsiUtilities.IsProductInstalledWithVersion(productCode, prodVersion));
54 }
55
56 public void DeleteTestRegistryValue(string name)
57 {
58 using (var root = this.TestContext.GetTestRegistryRoot(this.IsX64))
59 {
60 Assert.NotNull(root);
61 root.DeleteValue(name);
62 }
63 }
64
65 public void VerifyTestRegistryRootDeleted()
66 {
67 using var testRegistryRoot = this.TestContext.GetTestRegistryRoot(this.IsX64);
68 Assert.Null(testRegistryRoot);
69 }
70
71 public void VerifyTestRegistryValue(string name, string expectedValue)
72 {
73 using (var root = this.TestContext.GetTestRegistryRoot(this.IsX64))
74 {
75 Assert.NotNull(root);
76 var actualValue = root.GetValue(name) as string;
77 Assert.Equal(expectedValue, actualValue);
78 }
79 }
80 }
81}
diff --git a/src/test/burn/WixTestTools/TestTool.cs b/src/test/burn/WixTestTools/TestTool.cs
new file mode 100644
index 00000000..be5fde42
--- /dev/null
+++ b/src/test/burn/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/test/burn/WixTestTools/WixTestBase.cs b/src/test/burn/WixTestTools/WixTestBase.cs
new file mode 100644
index 00000000..bc050135
--- /dev/null
+++ b/src/test/burn/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/test/burn/WixTestTools/WixTestContext.cs b/src/test/burn/WixTestTools/WixTestContext.cs
new file mode 100644
index 00000000..a4e666f1
--- /dev/null
+++ b/src/test/burn/WixTestTools/WixTestContext.cs
@@ -0,0 +1,75 @@
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(bool x64, string additionalPath = null)
46 {
47 var baseDirectory = x64 ? Environment.SpecialFolder.ProgramFiles : Environment.SpecialFolder.ProgramFilesX86;
48 return Path.Combine(Environment.GetFolderPath(baseDirectory), "~Test WiX", this.TestGroupName, additionalPath ?? String.Empty);
49 }
50
51 /// <summary>
52 /// Gets the test registry key for the current test.
53 /// </summary>
54 /// <param name="additionalPath">Additional subkeys under the test registry key.</param>
55 /// <returns>Full path to the test registry key.</returns>
56 /// <remarks>
57 /// The package must write into HKLM\Software\WiX\Tests\[TestGroupName]\([Additional]).
58 /// </remarks>
59 public RegistryKey GetTestRegistryRoot(bool x64, string additionalPath = null)
60 {
61 var baseKey = x64 ? "Software" : @"Software\WOW6432Node";
62 var key = String.Format(@"{0}\WiX\Tests\{1}\{2}", baseKey, this.TestGroupName, additionalPath ?? String.Empty);
63 return Registry.LocalMachine.OpenSubKey(key, true);
64 }
65
66 private static ITest GetTest(ITestOutputHelper output)
67 {
68 // https://github.com/xunit/xunit/issues/416#issuecomment-378512739
69 var type = output.GetType();
70 var testMember = type.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic);
71 var test = (ITest)testMember.GetValue(output);
72 return test;
73 }
74 }
75}
diff --git a/src/test/burn/WixTestTools/WixTestTools.csproj b/src/test/burn/WixTestTools/WixTestTools.csproj
new file mode 100644
index 00000000..58f02be7
--- /dev/null
+++ b/src/test/burn/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.50" />
14 <PackageReference Include="WixToolset.Data" Version="4.0.218" />
15 <PackageReference Include="WixToolset.Mba.Core" Version="4.0.58" />
16 </ItemGroup>
17
18 <ItemGroup>
19 <PackageReference Include="xunit" Version="2.4.1" />
20 </ItemGroup>
21</Project>