aboutsummaryrefslogtreecommitdiff
path: root/src/Utilities
diff options
context:
space:
mode:
authorSean Hall <r.sean.hall@gmail.com>2020-12-01 23:02:05 -0600
committerSean Hall <r.sean.hall@gmail.com>2020-12-01 23:02:05 -0600
commit5d0434843c6f307a46fa95139c1e754221cc13af (patch)
tree559f1714b14c08fa09449b62770c9107fae968c3 /src/Utilities
parent86456524fe4640053616f6fc18311159e6fafea5 (diff)
downloadwix-5d0434843c6f307a46fa95139c1e754221cc13af.tar.gz
wix-5d0434843c6f307a46fa95139c1e754221cc13af.tar.bz2
wix-5d0434843c6f307a46fa95139c1e754221cc13af.zip
Add MSI transaction tests.
Diffstat (limited to 'src/Utilities')
-rw-r--r--src/Utilities/TestBA/Hresult.cs22
-rw-r--r--src/Utilities/TestBA/TestBA.BootstrapperCore.config18
-rw-r--r--src/Utilities/TestBA/TestBA.cs469
-rw-r--r--src/Utilities/TestBA/TestBA.csproj24
-rw-r--r--src/Utilities/TestBA/TestBAFactory.cs22
5 files changed, 555 insertions, 0 deletions
diff --git a/src/Utilities/TestBA/Hresult.cs b/src/Utilities/TestBA/Hresult.cs
new file mode 100644
index 00000000..bc1aa8c0
--- /dev/null
+++ b/src/Utilities/TestBA/Hresult.cs
@@ -0,0 +1,22 @@
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 WixToolset.Test.BA
4{
5 using System;
6
7 /// <summary>
8 /// Utility class to work with HRESULTs
9 /// </summary>
10 internal class Hresult
11 {
12 /// <summary>
13 /// Determines if an HRESULT was a success code or not.
14 /// </summary>
15 /// <param name="status">HRESULT to verify.</param>
16 /// <returns>True if the status is a success code.</returns>
17 public static bool Succeeded(int status)
18 {
19 return status >= 0;
20 }
21 }
22}
diff --git a/src/Utilities/TestBA/TestBA.BootstrapperCore.config b/src/Utilities/TestBA/TestBA.BootstrapperCore.config
new file mode 100644
index 00000000..55876a00
--- /dev/null
+++ b/src/Utilities/TestBA/TestBA.BootstrapperCore.config
@@ -0,0 +1,18 @@
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
5<configuration>
6 <configSections>
7 <sectionGroup name="wix.bootstrapper" type="WixToolset.Mba.Host.BootstrapperSectionGroup, WixToolset.Mba.Host">
8 <section name="host" type="WixToolset.Mba.Host.HostSection, WixToolset.Mba.Host" />
9 </sectionGroup>
10 </configSections>
11 <startup>
12 <supportedRuntime version="v4.0" />
13 <supportedRuntime version="v2.0.50727" />
14 </startup>
15 <wix.bootstrapper>
16 <host assemblyName="TestBA" />
17 </wix.bootstrapper>
18</configuration>
diff --git a/src/Utilities/TestBA/TestBA.cs b/src/Utilities/TestBA/TestBA.cs
new file mode 100644
index 00000000..e3305d33
--- /dev/null
+++ b/src/Utilities/TestBA/TestBA.cs
@@ -0,0 +1,469 @@
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 WixToolset.Test.BA
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using System.Threading;
10 using System.Windows.Forms;
11 using Microsoft.Win32;
12 using WixToolset.Mba.Core;
13
14 /// <summary>
15 /// A minimal UX used for testing.
16 /// </summary>
17 public class TestBA : BootstrapperApplication
18 {
19 private const string BurnBundleVersionVariable = "WixBundleVersion";
20
21 private ApplicationContext appContext;
22 private Form dummyWindow;
23 private LaunchAction action;
24 private int result;
25
26 private string updateBundlePath;
27
28 private int sleepDuringCache;
29 private int cancelCacheAtProgress;
30 private int sleepDuringExecute;
31 private int cancelExecuteAtProgress;
32 private int retryExecuteFilesInUse;
33
34 private IBootstrapperCommand Command { get; }
35
36 private IEngine Engine => this.engine;
37
38 /// <summary>
39 /// Initializes test user experience.
40 /// </summary>
41 public TestBA(IEngine engine, IBootstrapperCommand bootstrapperCommand)
42 : base(engine)
43 {
44 this.Command = bootstrapperCommand;
45 }
46
47 /// <summary>
48 /// Get the version of the install.
49 /// </summary>
50 public string Version { get; private set; }
51
52 /// <summary>
53 /// Indicates if DetectUpdate found a newer version to update.
54 /// </summary>
55 private bool UpdateAvailable { get; set; }
56
57 /// <summary>
58 /// UI Thread entry point for TestUX.
59 /// </summary>
60 protected override void Run()
61 {
62 this.action = this.Command.Action;
63 this.TestVariables();
64
65 this.Version = this.engine.GetVariableVersion(BurnBundleVersionVariable);
66 this.Log("Version: {0}", this.Version);
67
68 List<string> verifyArguments = this.ReadVerifyArguments();
69
70 foreach (string arg in this.Command.CommandLineArgs)
71 {
72 // If we're not in the update already, process the updatebundle.
73 if (this.Command.Relation != RelationType.Update && arg.StartsWith("-updatebundle:", StringComparison.OrdinalIgnoreCase))
74 {
75 this.updateBundlePath = arg.Substring(14);
76 FileInfo info = new FileInfo(this.updateBundlePath);
77 this.Engine.SetUpdate(this.updateBundlePath, null, info.Length, UpdateHashType.None, null);
78 this.UpdateAvailable = true;
79 this.action = LaunchAction.UpdateReplaceEmbedded;
80 }
81 else if (this.Command.Relation != RelationType.Update && arg.StartsWith("-checkupdate", StringComparison.OrdinalIgnoreCase))
82 {
83 this.action = LaunchAction.UpdateReplace;
84 }
85
86 verifyArguments.Remove(arg);
87 }
88 this.Log("Action: {0}", this.action);
89
90 // If there are any verification arguments left, error out.
91 if (0 < verifyArguments.Count)
92 {
93 foreach (string expectedArg in verifyArguments)
94 {
95 this.Log("Failure. Expected command-line to have argument: {0}", expectedArg);
96 }
97
98 this.Engine.Quit(-1);
99 return;
100 }
101
102 this.dummyWindow = new Form();
103 this.dummyWindow.CreateControl();
104 this.appContext = new ApplicationContext();
105
106 int redetectCount = 0;
107 string redetect = this.ReadPackageAction(null, "RedetectCount");
108 if (String.IsNullOrEmpty(redetect) || !Int32.TryParse(redetect, out redetectCount))
109 {
110 redetectCount = 0;
111 }
112
113 do
114 {
115 this.Engine.Detect();
116 this.Log("Completed detection phase: {0} re-runs remaining", redetectCount);
117 } while (0 < redetectCount--);
118
119 Application.Run(this.appContext);
120 this.Engine.Quit(this.result & 0xFFFF); // return plain old Win32 error, not HRESULT.
121 }
122
123 protected override void OnDetectUpdateBegin(DetectUpdateBeginEventArgs args)
124 {
125 this.Log("OnDetectUpdateBegin");
126 if ((LaunchAction.UpdateReplaceEmbedded == this.action)|(LaunchAction.UpdateReplace == this.action))
127 {
128 args.Skip = false;
129 }
130 }
131
132 protected override void OnDetectUpdate(DetectUpdateEventArgs e)
133 {
134 // The list of updates is sorted in descending version, so the first callback should be the largest update available.
135 // This update should be either larger than ours (so we are out of date), the same as ours (so we are current)
136 // or smaller than ours (we have a private build). If we really wanted to, we could leave the e.StopProcessingUpdates alone and
137 // enumerate all of the updates.
138 this.Log(String.Format("Potential update v{0} from '{1}'; current version: v{2}", e.Version, e.UpdateLocation, this.Version));
139 if (this.Engine.CompareVersions(e.Version, this.Version) > 0)
140 {
141 this.Log(String.Format("Selected update v{0}", e.Version));
142 this.Engine.SetUpdate(null, e.UpdateLocation, e.Size, UpdateHashType.None, null);
143 this.UpdateAvailable = true;
144 }
145 else
146 {
147 this.UpdateAvailable = false;
148 }
149 e.StopProcessingUpdates = true;
150 }
151
152 protected override void OnDetectUpdateComplete(DetectUpdateCompleteEventArgs e)
153 {
154 this.Log("OnDetectUpdateComplete");
155
156 // Failed to process an update, allow the existing bundle to still install.
157 if (!Hresult.Succeeded(e.Status))
158 {
159 this.Log(String.Format("Failed to locate an update, status of 0x{0:X8}, updates disabled.", e.Status));
160 e.IgnoreError = true; // But continue on...
161 }
162 }
163
164 protected override void OnDetectComplete(DetectCompleteEventArgs args)
165 {
166 this.result = args.Status;
167
168 if (Hresult.Succeeded(this.result) && (this.UpdateAvailable | (!((LaunchAction.UpdateReplaceEmbedded == this.action) | (LaunchAction.UpdateReplace == this.action)))))
169 {
170 this.Engine.Plan(this.action);
171 }
172 else
173 {
174 this.appContext.ExitThread();
175 }
176 }
177
178 protected override void OnPlanPackageBegin(PlanPackageBeginEventArgs args)
179 {
180 RequestState state;
181 string action = this.ReadPackageAction(args.PackageId, "Requested");
182 if (TryParseEnum<RequestState>(action, out state))
183 {
184 args.State = state;
185 }
186 }
187
188 protected override void OnPlanTargetMsiPackage(PlanTargetMsiPackageEventArgs args)
189 {
190 RequestState state;
191 string action = this.ReadPackageAction(args.PackageId, "Requested");
192 if (TryParseEnum<RequestState>(action, out state))
193 {
194 args.State = state;
195 }
196 }
197
198 protected override void OnPlanMsiFeature(PlanMsiFeatureEventArgs args)
199 {
200 FeatureState state;
201 string action = this.ReadFeatureAction(args.PackageId, args.FeatureId, "Requested");
202 if (TryParseEnum<FeatureState>(action, out state))
203 {
204 args.State = state;
205 }
206 }
207
208 protected override void OnPlanComplete(PlanCompleteEventArgs args)
209 {
210 this.result = args.Status;
211 if (Hresult.Succeeded(this.result))
212 {
213 this.Engine.Apply(this.dummyWindow.Handle);
214 }
215 else
216 {
217 this.appContext.ExitThread();
218 }
219 }
220
221 protected override void OnCachePackageBegin(CachePackageBeginEventArgs args)
222 {
223 this.Log("OnCachePackageBegin() - package: {0}, payloads to cache: {1}", args.PackageId, args.CachePayloads);
224
225 string slowProgress = this.ReadPackageAction(args.PackageId, "SlowCache");
226 if (String.IsNullOrEmpty(slowProgress) || !Int32.TryParse(slowProgress, out this.sleepDuringCache))
227 {
228 this.sleepDuringCache = 0;
229 }
230
231 string cancelCache = this.ReadPackageAction(args.PackageId, "CancelCacheAtProgress");
232 if (String.IsNullOrEmpty(cancelCache) || !Int32.TryParse(cancelCache, out this.cancelCacheAtProgress))
233 {
234 this.cancelCacheAtProgress = -1;
235 }
236 }
237
238 protected override void OnCacheAcquireProgress(CacheAcquireProgressEventArgs args)
239 {
240 this.Log("OnCacheAcquireProgress() - container/package: {0}, payload: {1}, progress: {2}, total: {3}, overall progress: {4}%", args.PackageOrContainerId, args.PayloadId, args.Progress, args.Total, args.OverallPercentage);
241
242 if (this.cancelCacheAtProgress > 0 && this.cancelCacheAtProgress <= args.Progress)
243 {
244 args.Cancel = true;
245 }
246 else if (this.sleepDuringCache > 0)
247 {
248 Thread.Sleep(this.sleepDuringCache);
249 }
250 }
251
252 protected override void OnExecutePackageBegin(ExecutePackageBeginEventArgs args)
253 {
254 this.Log("OnExecutePackageBegin() - package: {0}, rollback: {1}", args.PackageId, !args.ShouldExecute);
255
256 string slowProgress = this.ReadPackageAction(args.PackageId, "SlowExecute");
257 if (String.IsNullOrEmpty(slowProgress) || !Int32.TryParse(slowProgress, out this.sleepDuringExecute))
258 {
259 this.sleepDuringExecute = 0;
260 }
261
262 string cancelExecute = this.ReadPackageAction(args.PackageId, "CancelExecuteAtProgress");
263 if (String.IsNullOrEmpty(cancelExecute) || !Int32.TryParse(cancelExecute, out this.cancelExecuteAtProgress))
264 {
265 this.cancelExecuteAtProgress = -1;
266 }
267
268 string retryBeforeCancel = this.ReadPackageAction(args.PackageId, "RetryExecuteFilesInUse");
269 if (String.IsNullOrEmpty(retryBeforeCancel) || !Int32.TryParse(retryBeforeCancel, out this.retryExecuteFilesInUse))
270 {
271 this.retryExecuteFilesInUse = 0;
272 }
273 }
274
275 protected override void OnExecuteFilesInUse(ExecuteFilesInUseEventArgs args)
276 {
277 this.Log("OnExecuteFilesInUse() - package: {0}, retries remaining: {1}, data: {2}", args.PackageId, this.retryExecuteFilesInUse, String.Join(", ", args.Files.ToArray()));
278
279 if (this.retryExecuteFilesInUse > 0)
280 {
281 --this.retryExecuteFilesInUse;
282 args.Result = Result.Retry;
283 }
284 else
285 {
286 args.Result = Result.Abort;
287 }
288 }
289
290 protected override void OnExecuteProgress(ExecuteProgressEventArgs args)
291 {
292 this.Log("OnExecuteProgress() - package: {0}, progress: {1}%, overall progress: {2}%", args.PackageId, args.ProgressPercentage, args.OverallPercentage);
293
294 if (this.cancelExecuteAtProgress > 0 && this.cancelExecuteAtProgress <= args.ProgressPercentage)
295 {
296 args.Cancel = true;
297 }
298 else if (this.sleepDuringExecute > 0)
299 {
300 Thread.Sleep(this.sleepDuringExecute);
301 }
302 }
303
304 protected override void OnExecutePatchTarget(ExecutePatchTargetEventArgs args)
305 {
306 this.Log("OnExecutePatchTarget - Patch Package: {0}, Target Product Code: {1}", args.PackageId, args.TargetProductCode);
307 }
308
309 protected override void OnProgress(ProgressEventArgs args)
310 {
311 this.Log("OnProgress() - progress: {0}%, overall progress: {1}%", args.ProgressPercentage, args.OverallPercentage);
312 if (this.Command.Display == Display.Embedded)
313 {
314 this.Engine.SendEmbeddedProgress(args.ProgressPercentage, args.OverallPercentage);
315 }
316 }
317
318 protected override void OnResolveSource(ResolveSourceEventArgs args)
319 {
320 if (!String.IsNullOrEmpty(args.DownloadSource))
321 {
322 args.Action = BOOTSTRAPPER_RESOLVESOURCE_ACTION.Download;
323 }
324 }
325
326 protected override void OnApplyComplete(ApplyCompleteEventArgs args)
327 {
328 // Output what the privileges are now.
329 this.Log("After elevation: WixBundleElevated = {0}", this.Engine.GetVariableNumeric("WixBundleElevated"));
330
331 this.result = args.Status;
332 this.appContext.ExitThread();
333 }
334
335 protected override void OnSystemShutdown(SystemShutdownEventArgs args)
336 {
337 // Always prevent shutdown.
338 this.Log("Disallowed system request to shut down the bootstrapper application.");
339 args.Cancel = true;
340
341 this.appContext.ExitThread();
342 }
343
344 private void TestVariables()
345 {
346 // First make sure we can check and get standard variables of each type.
347 {
348 string value = null;
349 if (this.Engine.ContainsVariable("WindowsFolder"))
350 {
351 value = this.Engine.GetVariableString("WindowsFolder");
352 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a string variable: WindowsFolder");
353 }
354 else
355 {
356 throw new Exception("Engine did not define a standard variable: WindowsFolder");
357 }
358 }
359
360 {
361 long value = 0;
362 if (this.Engine.ContainsVariable("NTProductType"))
363 {
364 value = this.Engine.GetVariableNumeric("NTProductType");
365 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a numeric variable: NTProductType");
366 }
367 else
368 {
369 throw new Exception("Engine did not define a standard variable: NTProductType");
370 }
371 }
372
373 {
374 string value = null;
375 if (this.Engine.ContainsVariable("VersionMsi"))
376 {
377 value = this.Engine.GetVariableVersion("VersionMsi");
378 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a version variable: VersionMsi");
379 }
380 else
381 {
382 throw new Exception("Engine did not define a standard variable: VersionMsi");
383 }
384 }
385
386 // Now validate that Contians returns false for non-existant variables of each type.
387 if (this.Engine.ContainsVariable("TestStringVariableShouldNotExist"))
388 {
389 throw new Exception("Engine defined a variable that should not exist: TestStringVariableShouldNotExist");
390 }
391 else
392 {
393 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent string variable: TestStringVariableShouldNotExist");
394 }
395
396 if (this.Engine.ContainsVariable("TestNumericVariableShouldNotExist"))
397 {
398 throw new Exception("Engine defined a variable that should not exist: TestNumericVariableShouldNotExist");
399 }
400 else
401 {
402 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent numeric variable: TestNumericVariableShouldNotExist");
403 }
404
405 if (this.Engine.ContainsVariable("TestVersionVariableShouldNotExist"))
406 {
407 throw new Exception("Engine defined a variable that should not exist: TestVersionVariableShouldNotExist");
408 }
409 else
410 {
411 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent version variable: TestVersionVariableShouldNotExist");
412 }
413
414 // Output what the initially run privileges were.
415 this.Engine.Log(LogLevel.Verbose, String.Format("TEST: WixBundleElevated = {0}", this.Engine.GetVariableNumeric("WixBundleElevated")));
416 }
417
418 private void Log(string format, params object[] args)
419 {
420 string relation = this.Command.Relation != RelationType.None ? String.Concat(" (", this.Command.Relation.ToString().ToLowerInvariant(), ")") : String.Empty;
421 string message = String.Format(format, args);
422
423 this.Engine.Log(LogLevel.Standard, String.Concat("TESTBA", relation, ": ", message));
424 }
425
426 private List<string> ReadVerifyArguments()
427 {
428 string testName = this.Engine.GetVariableString("TestGroupName");
429 using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}", testName)))
430 {
431 string verifyArguments = testKey == null ? null : testKey.GetValue("VerifyArguments") as string;
432 return verifyArguments == null ? new List<string>() : new List<string>(verifyArguments.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
433 }
434 }
435
436 private string ReadPackageAction(string packageId, string state)
437 {
438 string testName = this.Engine.GetVariableString("TestGroupName");
439 using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}\{1}", testName, String.IsNullOrEmpty(packageId) ? String.Empty : packageId)))
440 {
441 return testKey == null ? null : testKey.GetValue(state) as string;
442 }
443 }
444
445 private string ReadFeatureAction(string packageId, string featureId, string state)
446 {
447 string testName = this.Engine.GetVariableString("TestGroupName");
448 using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}\{1}", testName, packageId)))
449 {
450 string registryName = String.Concat(featureId, state);
451 return testKey == null ? null : testKey.GetValue(registryName) as string;
452 }
453 }
454
455 private static bool TryParseEnum<T>(string value, out T t)
456 {
457 try
458 {
459 t = (T)Enum.Parse(typeof(T), value, true);
460 return true;
461 }
462 catch (ArgumentException) { }
463 catch (OverflowException) { }
464
465 t = default(T);
466 return false;
467 }
468 }
469}
diff --git a/src/Utilities/TestBA/TestBA.csproj b/src/Utilities/TestBA/TestBA.csproj
new file mode 100644
index 00000000..ad7a59b4
--- /dev/null
+++ b/src/Utilities/TestBA/TestBA.csproj
@@ -0,0 +1,24 @@
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>net35</TargetFramework>
7 <AssemblyName>TestBA</AssemblyName>
8 <RootNamespace>WixToolset.Test.BA</RootNamespace>
9 <DebugType>embedded</DebugType>
10 <RuntimeIdentifier>win-x86</RuntimeIdentifier>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <Content Include="TestBA.BootstrapperCore.config" CopyToOutputDirectory="PreserveNewest" />
15 </ItemGroup>
16
17 <ItemGroup>
18 <PackageReference Include="WixToolset.Mba.Core" Version="4.0.41" />
19 </ItemGroup>
20
21 <ItemGroup>
22 <Reference Include="System.Windows.Forms" />
23 </ItemGroup>
24</Project> \ No newline at end of file
diff --git a/src/Utilities/TestBA/TestBAFactory.cs b/src/Utilities/TestBA/TestBAFactory.cs
new file mode 100644
index 00000000..ba1de367
--- /dev/null
+++ b/src/Utilities/TestBA/TestBAFactory.cs
@@ -0,0 +1,22 @@
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
3[assembly: WixToolset.Mba.Core.BootstrapperApplicationFactory(typeof(WixToolset.Test.BA.TestBAFactory))]
4namespace WixToolset.Test.BA
5{
6 using WixToolset.Mba.Core;
7
8 public class TestBAFactory : BaseBootstrapperApplicationFactory
9 {
10 private static int loadCount = 0;
11
12 protected override IBootstrapperApplication Create(IEngine engine, IBootstrapperCommand bootstrapperCommand)
13 {
14 if (loadCount > 0)
15 {
16 engine.Log(LogLevel.Standard, $"Reloaded {loadCount} time(s)");
17 }
18 ++loadCount;
19 return new TestBA(engine, bootstrapperCommand);
20 }
21 }
22}