aboutsummaryrefslogtreecommitdiff
path: root/src/TestBA
diff options
context:
space:
mode:
authorSean Hall <r.sean.hall@gmail.com>2021-02-17 15:45:19 -0600
committerSean Hall <r.sean.hall@gmail.com>2021-02-17 16:20:10 -0600
commitbadde27bf66fcacef2d625af0bd7590ecdc5804f (patch)
treec70f7da4642b2c9d2613a575fbb0169bc3034a60 /src/TestBA
parente950ce317301aea2e9c8cdab1bf8c453c40a4edd (diff)
downloadwix-badde27bf66fcacef2d625af0bd7590ecdc5804f.tar.gz
wix-badde27bf66fcacef2d625af0bd7590ecdc5804f.tar.bz2
wix-badde27bf66fcacef2d625af0bd7590ecdc5804f.zip
Create WixTestTools project and reorganize files.
Diffstat (limited to 'src/TestBA')
-rw-r--r--src/TestBA/Hresult.cs22
-rw-r--r--src/TestBA/TestBA.BootstrapperCore.config18
-rw-r--r--src/TestBA/TestBA.cs584
-rw-r--r--src/TestBA/TestBA.csproj24
-rw-r--r--src/TestBA/TestBAFactory.cs22
5 files changed, 670 insertions, 0 deletions
diff --git a/src/TestBA/Hresult.cs b/src/TestBA/Hresult.cs
new file mode 100644
index 00000000..bc1aa8c0
--- /dev/null
+++ b/src/TestBA/Hresult.cs
@@ -0,0 +1,22 @@
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/TestBA/TestBA.BootstrapperCore.config b/src/TestBA/TestBA.BootstrapperCore.config
new file mode 100644
index 00000000..55876a00
--- /dev/null
+++ b/src/TestBA/TestBA.BootstrapperCore.config
@@ -0,0 +1,18 @@
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/TestBA/TestBA.cs b/src/TestBA/TestBA.cs
new file mode 100644
index 00000000..1348ce98
--- /dev/null
+++ b/src/TestBA/TestBA.cs
@@ -0,0 +1,584 @@
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 Form dummyWindow;
22 private IntPtr windowHandle;
23 private LaunchAction action;
24 private ManualResetEvent wait;
25 private int result;
26
27 private string updateBundlePath;
28
29 private bool immediatelyQuit;
30 private bool quitAfterDetect;
31 private int redetectRemaining;
32 private int sleepDuringCache;
33 private int cancelCacheAtProgress;
34 private int sleepDuringExecute;
35 private int cancelExecuteAtProgress;
36 private string cancelExecuteActionName;
37 private int cancelOnProgressAtProgress;
38 private int retryExecuteFilesInUse;
39
40 private IBootstrapperCommand Command { get; }
41
42 private IEngine Engine => this.engine;
43
44 /// <summary>
45 /// Initializes test user experience.
46 /// </summary>
47 public TestBA(IEngine engine, IBootstrapperCommand bootstrapperCommand)
48 : base(engine)
49 {
50 this.Command = bootstrapperCommand;
51 this.wait = new ManualResetEvent(false);
52 }
53
54 /// <summary>
55 /// Get the version of the install.
56 /// </summary>
57 public string Version { get; private set; }
58
59 /// <summary>
60 /// Indicates if DetectUpdate found a newer version to update.
61 /// </summary>
62 private bool UpdateAvailable { get; set; }
63
64 /// <summary>
65 /// UI Thread entry point for TestUX.
66 /// </summary>
67 protected override void OnStartup(StartupEventArgs args)
68 {
69 string immediatelyQuit = this.ReadPackageAction(null, "ImmediatelyQuit");
70 if (!String.IsNullOrEmpty(immediatelyQuit) && Boolean.TryParse(immediatelyQuit, out this.immediatelyQuit) && this.immediatelyQuit)
71 {
72 this.Engine.Quit(0);
73 return;
74 }
75
76 base.OnStartup(args);
77
78 this.action = this.Command.Action;
79 this.TestVariables();
80
81 this.Version = this.engine.GetVariableVersion(BurnBundleVersionVariable);
82 this.Log("Version: {0}", this.Version);
83
84 List<string> verifyArguments = this.ReadVerifyArguments();
85
86 foreach (string arg in this.Command.CommandLineArgs)
87 {
88 // If we're not in the update already, process the updatebundle.
89 if (this.Command.Relation != RelationType.Update && arg.StartsWith("-updatebundle:", StringComparison.OrdinalIgnoreCase))
90 {
91 this.updateBundlePath = arg.Substring(14);
92 FileInfo info = new FileInfo(this.updateBundlePath);
93 this.Engine.SetUpdate(this.updateBundlePath, null, info.Length, UpdateHashType.None, null);
94 this.UpdateAvailable = true;
95 this.action = LaunchAction.UpdateReplaceEmbedded;
96 }
97 else if (this.Command.Relation != RelationType.Update && arg.StartsWith("-checkupdate", StringComparison.OrdinalIgnoreCase))
98 {
99 this.action = LaunchAction.UpdateReplace;
100 }
101
102 verifyArguments.Remove(arg);
103 }
104 this.Log("Action: {0}", this.action);
105
106 // If there are any verification arguments left, error out.
107 if (0 < verifyArguments.Count)
108 {
109 foreach (string expectedArg in verifyArguments)
110 {
111 this.Log("Failure. Expected command-line to have argument: {0}", expectedArg);
112 }
113
114 this.Engine.Quit(-1);
115 return;
116 }
117
118 int redetectCount;
119 string redetect = this.ReadPackageAction(null, "RedetectCount");
120 if (String.IsNullOrEmpty(redetect) || !Int32.TryParse(redetect, out redetectCount))
121 {
122 redetectCount = 0;
123 }
124
125 string quitAfterDetect = this.ReadPackageAction(null, "QuitAfterDetect");
126 if (String.IsNullOrEmpty(quitAfterDetect) || !Boolean.TryParse(quitAfterDetect, out this.quitAfterDetect))
127 {
128 this.quitAfterDetect = false;
129 }
130
131 this.wait.WaitOne();
132
133 this.redetectRemaining = redetectCount;
134 for (int i = -1; i < redetectCount; i++)
135 {
136 this.Engine.Detect(this.windowHandle);
137 }
138 }
139
140 protected override void Run()
141 {
142 this.dummyWindow = new Form();
143 this.windowHandle = this.dummyWindow.Handle;
144
145 this.Log("Running TestBA application");
146 this.wait.Set();
147 Application.Run();
148 }
149
150 private void ShutdownUiThread()
151 {
152 if (this.dummyWindow != null)
153 {
154 this.dummyWindow.Invoke(new Action(Application.ExitThread));
155 this.dummyWindow.Dispose();
156 }
157
158 this.Engine.Quit(this.result & 0xFFFF); // return plain old Win32 error, not HRESULT.
159 }
160
161 protected override void OnDetectUpdateBegin(DetectUpdateBeginEventArgs args)
162 {
163 this.Log("OnDetectUpdateBegin");
164 if (LaunchAction.UpdateReplaceEmbedded == this.action || LaunchAction.UpdateReplace == this.action)
165 {
166 args.Skip = false;
167 }
168 }
169
170 protected override void OnDetectUpdate(DetectUpdateEventArgs e)
171 {
172 // The list of updates is sorted in descending version, so the first callback should be the largest update available.
173 // This update should be either larger than ours (so we are out of date), the same as ours (so we are current)
174 // or smaller than ours (we have a private build). If we really wanted to, we could leave the e.StopProcessingUpdates alone and
175 // enumerate all of the updates.
176 this.Log(String.Format("Potential update v{0} from '{1}'; current version: v{2}", e.Version, e.UpdateLocation, this.Version));
177 if (this.Engine.CompareVersions(e.Version, this.Version) > 0)
178 {
179 this.Log(String.Format("Selected update v{0}", e.Version));
180 this.Engine.SetUpdate(null, e.UpdateLocation, e.Size, UpdateHashType.None, null);
181 this.UpdateAvailable = true;
182 }
183 else
184 {
185 this.UpdateAvailable = false;
186 }
187 e.StopProcessingUpdates = true;
188 }
189
190 protected override void OnDetectUpdateComplete(DetectUpdateCompleteEventArgs e)
191 {
192 this.Log("OnDetectUpdateComplete");
193
194 // Failed to process an update, allow the existing bundle to still install.
195 if (!Hresult.Succeeded(e.Status))
196 {
197 this.Log(String.Format("Failed to locate an update, status of 0x{0:X8}, updates disabled.", e.Status));
198 e.IgnoreError = true; // But continue on...
199 }
200 }
201
202 protected override void OnDetectComplete(DetectCompleteEventArgs args)
203 {
204 this.result = args.Status;
205
206 if (Hresult.Succeeded(this.result) &&
207 (this.UpdateAvailable || LaunchAction.UpdateReplaceEmbedded != this.action && LaunchAction.UpdateReplace != this.action))
208 {
209 if (this.redetectRemaining > 0)
210 {
211 this.Log("Completed detection phase: {0} re-runs remaining", this.redetectRemaining--);
212 }
213 else if (this.quitAfterDetect)
214 {
215 this.ShutdownUiThread();
216 }
217 else
218 {
219 this.Engine.Plan(this.action);
220 }
221 }
222 else
223 {
224 this.ShutdownUiThread();
225 }
226 }
227
228 protected override void OnPlanPackageBegin(PlanPackageBeginEventArgs args)
229 {
230 RequestState state;
231 string action = this.ReadPackageAction(args.PackageId, "Requested");
232 if (TryParseEnum<RequestState>(action, out state))
233 {
234 args.State = state;
235 }
236 }
237
238 protected override void OnPlanTargetMsiPackage(PlanTargetMsiPackageEventArgs args)
239 {
240 RequestState state;
241 string action = this.ReadPackageAction(args.PackageId, "Requested");
242 if (TryParseEnum<RequestState>(action, out state))
243 {
244 args.State = state;
245 }
246 }
247
248 protected override void OnPlanMsiFeature(PlanMsiFeatureEventArgs args)
249 {
250 FeatureState state;
251 string action = this.ReadFeatureAction(args.PackageId, args.FeatureId, "Requested");
252 if (TryParseEnum<FeatureState>(action, out state))
253 {
254 args.State = state;
255 }
256 }
257
258 protected override void OnPlanComplete(PlanCompleteEventArgs args)
259 {
260 this.result = args.Status;
261 if (Hresult.Succeeded(this.result))
262 {
263 this.Engine.Apply(this.windowHandle);
264 }
265 else
266 {
267 this.ShutdownUiThread();
268 }
269 }
270
271 protected override void OnCachePackageBegin(CachePackageBeginEventArgs args)
272 {
273 this.Log("OnCachePackageBegin() - package: {0}, payloads to cache: {1}", args.PackageId, args.CachePayloads);
274
275 string slowProgress = this.ReadPackageAction(args.PackageId, "SlowCache");
276 if (String.IsNullOrEmpty(slowProgress) || !Int32.TryParse(slowProgress, out this.sleepDuringCache))
277 {
278 this.sleepDuringCache = 0;
279 }
280 else
281 {
282 this.Log(" SlowCache: {0}", this.sleepDuringCache);
283 }
284
285 string cancelCache = this.ReadPackageAction(args.PackageId, "CancelCacheAtProgress");
286 if (String.IsNullOrEmpty(cancelCache) || !Int32.TryParse(cancelCache, out this.cancelCacheAtProgress))
287 {
288 this.cancelCacheAtProgress = -1;
289 }
290 else
291 {
292 this.Log(" CancelCacheAtProgress: {0}", this.cancelCacheAtProgress);
293 }
294 }
295
296 protected override void OnCacheAcquireProgress(CacheAcquireProgressEventArgs args)
297 {
298 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);
299
300 if (this.cancelCacheAtProgress >= 0 && this.cancelCacheAtProgress <= args.Progress)
301 {
302 args.Cancel = true;
303 this.Log("OnCacheAcquireProgress(cancel)");
304 }
305 else if (this.sleepDuringCache > 0)
306 {
307 this.Log("OnCacheAcquireProgress(sleep {0})", this.sleepDuringCache);
308 Thread.Sleep(this.sleepDuringCache);
309 }
310 }
311
312 protected override void OnExecutePackageBegin(ExecutePackageBeginEventArgs args)
313 {
314 this.Log("OnExecutePackageBegin() - package: {0}, rollback: {1}", args.PackageId, !args.ShouldExecute);
315
316 string slowProgress = this.ReadPackageAction(args.PackageId, "SlowExecute");
317 if (String.IsNullOrEmpty(slowProgress) || !Int32.TryParse(slowProgress, out this.sleepDuringExecute))
318 {
319 this.sleepDuringExecute = 0;
320 }
321 else
322 {
323 this.Log(" SlowExecute: {0}", this.sleepDuringExecute);
324 }
325
326 string cancelExecute = this.ReadPackageAction(args.PackageId, "CancelExecuteAtProgress");
327 if (String.IsNullOrEmpty(cancelExecute) || !Int32.TryParse(cancelExecute, out this.cancelExecuteAtProgress))
328 {
329 this.cancelExecuteAtProgress = -1;
330 }
331 else
332 {
333 this.Log(" CancelExecuteAtProgress: {0}", this.cancelExecuteAtProgress);
334 }
335
336 this.cancelExecuteActionName = this.ReadPackageAction(args.PackageId, "CancelExecuteAtActionStart");
337 if (!String.IsNullOrEmpty(this.cancelExecuteActionName))
338 {
339 this.Log(" CancelExecuteAtActionState: {0}", this.cancelExecuteActionName);
340 }
341
342 string cancelOnProgressAtProgress = this.ReadPackageAction(args.PackageId, "CancelOnProgressAtProgress");
343 if (String.IsNullOrEmpty(cancelOnProgressAtProgress) || !Int32.TryParse(cancelOnProgressAtProgress, out this.cancelOnProgressAtProgress))
344 {
345 this.cancelOnProgressAtProgress = -1;
346 }
347 else
348 {
349 this.Log(" CancelOnProgressAtProgress: {0}", this.cancelOnProgressAtProgress);
350 }
351
352 string retryBeforeCancel = this.ReadPackageAction(args.PackageId, "RetryExecuteFilesInUse");
353 if (String.IsNullOrEmpty(retryBeforeCancel) || !Int32.TryParse(retryBeforeCancel, out this.retryExecuteFilesInUse))
354 {
355 this.retryExecuteFilesInUse = 0;
356 }
357 else
358 {
359 this.Log(" RetryExecuteFilesInUse: {0}", this.retryExecuteFilesInUse);
360 }
361 }
362
363 protected override void OnExecuteFilesInUse(ExecuteFilesInUseEventArgs args)
364 {
365 this.Log("OnExecuteFilesInUse() - package: {0}, retries remaining: {1}, data: {2}", args.PackageId, this.retryExecuteFilesInUse, String.Join(", ", args.Files.ToArray()));
366
367 if (this.retryExecuteFilesInUse > 0)
368 {
369 --this.retryExecuteFilesInUse;
370 args.Result = Result.Retry;
371 }
372 else
373 {
374 args.Result = Result.Abort;
375 }
376 }
377
378 protected override void OnExecuteMsiMessage(ExecuteMsiMessageEventArgs args)
379 {
380 this.Log("OnExecuteMsiMessage() - MessageType: {0}, Message: {1}, Data: '{2}'", args.MessageType, args.Message, String.Join("','", args.Data.ToArray()));
381
382 if (!String.IsNullOrEmpty(this.cancelExecuteActionName) && args.MessageType == InstallMessage.ActionStart &&
383 args.Data.Count > 0 && args.Data[0] == this.cancelExecuteActionName)
384 {
385 this.Log("OnExecuteMsiMessage(cancelNextProgress)");
386 this.cancelExecuteAtProgress = 0;
387 }
388 }
389
390 protected override void OnExecuteProgress(ExecuteProgressEventArgs args)
391 {
392 this.Log("OnExecuteProgress() - package: {0}, progress: {1}%, overall progress: {2}%", args.PackageId, args.ProgressPercentage, args.OverallPercentage);
393
394 if (this.cancelExecuteAtProgress >= 0 && this.cancelExecuteAtProgress <= args.ProgressPercentage)
395 {
396 args.Cancel = true;
397 this.Log("OnExecuteProgress(cancel)");
398 }
399 else if (this.sleepDuringExecute > 0)
400 {
401 this.Log("OnExecuteProgress(sleep {0})", this.sleepDuringExecute);
402 Thread.Sleep(this.sleepDuringExecute);
403 }
404 }
405
406 protected override void OnExecutePatchTarget(ExecutePatchTargetEventArgs args)
407 {
408 this.Log("OnExecutePatchTarget - Patch Package: {0}, Target Product Code: {1}", args.PackageId, args.TargetProductCode);
409 }
410
411 protected override void OnProgress(ProgressEventArgs args)
412 {
413 this.Log("OnProgress() - progress: {0}%, overall progress: {1}%", args.ProgressPercentage, args.OverallPercentage);
414 if (this.Command.Display == Display.Embedded)
415 {
416 this.Engine.SendEmbeddedProgress(args.ProgressPercentage, args.OverallPercentage);
417 }
418
419 if (this.cancelOnProgressAtProgress >= 0 && this.cancelOnProgressAtProgress <= args.OverallPercentage)
420 {
421 args.Cancel = true;
422 this.Log("OnProgress(cancel)");
423 }
424 }
425
426 protected override void OnResolveSource(ResolveSourceEventArgs args)
427 {
428 if (!String.IsNullOrEmpty(args.DownloadSource))
429 {
430 args.Action = BOOTSTRAPPER_RESOLVESOURCE_ACTION.Download;
431 }
432 }
433
434 protected override void OnApplyBegin(ApplyBeginEventArgs args)
435 {
436 this.cancelOnProgressAtProgress = -1;
437 this.cancelExecuteAtProgress = -1;
438 this.cancelCacheAtProgress = -1;
439 }
440
441 protected override void OnApplyComplete(ApplyCompleteEventArgs args)
442 {
443 // Output what the privileges are now.
444 this.Log("After elevation: WixBundleElevated = {0}", this.Engine.GetVariableNumeric("WixBundleElevated"));
445
446 this.result = args.Status;
447 this.ShutdownUiThread();
448 }
449
450 protected override void OnSystemShutdown(SystemShutdownEventArgs args)
451 {
452 // Always prevent shutdown.
453 this.Log("Disallowed system request to shut down the bootstrapper application.");
454 args.Cancel = true;
455
456 this.ShutdownUiThread();
457 }
458
459 private void TestVariables()
460 {
461 // First make sure we can check and get standard variables of each type.
462 {
463 string value = null;
464 if (this.Engine.ContainsVariable("WindowsFolder"))
465 {
466 value = this.Engine.GetVariableString("WindowsFolder");
467 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a string variable: WindowsFolder");
468 }
469 else
470 {
471 throw new Exception("Engine did not define a standard variable: WindowsFolder");
472 }
473 }
474
475 {
476 long value = 0;
477 if (this.Engine.ContainsVariable("NTProductType"))
478 {
479 value = this.Engine.GetVariableNumeric("NTProductType");
480 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a numeric variable: NTProductType");
481 }
482 else
483 {
484 throw new Exception("Engine did not define a standard variable: NTProductType");
485 }
486 }
487
488 {
489 string value = null;
490 if (this.Engine.ContainsVariable("VersionMsi"))
491 {
492 value = this.Engine.GetVariableVersion("VersionMsi");
493 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully retrieved a version variable: VersionMsi");
494 }
495 else
496 {
497 throw new Exception("Engine did not define a standard variable: VersionMsi");
498 }
499 }
500
501 // Now validate that Contians returns false for non-existant variables of each type.
502 if (this.Engine.ContainsVariable("TestStringVariableShouldNotExist"))
503 {
504 throw new Exception("Engine defined a variable that should not exist: TestStringVariableShouldNotExist");
505 }
506 else
507 {
508 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent string variable: TestStringVariableShouldNotExist");
509 }
510
511 if (this.Engine.ContainsVariable("TestNumericVariableShouldNotExist"))
512 {
513 throw new Exception("Engine defined a variable that should not exist: TestNumericVariableShouldNotExist");
514 }
515 else
516 {
517 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent numeric variable: TestNumericVariableShouldNotExist");
518 }
519
520 if (this.Engine.ContainsVariable("TestVersionVariableShouldNotExist"))
521 {
522 throw new Exception("Engine defined a variable that should not exist: TestVersionVariableShouldNotExist");
523 }
524 else
525 {
526 this.Engine.Log(LogLevel.Verbose, "TEST: Successfully checked for non-existent version variable: TestVersionVariableShouldNotExist");
527 }
528
529 // Output what the initially run privileges were.
530 this.Engine.Log(LogLevel.Verbose, String.Format("TEST: WixBundleElevated = {0}", this.Engine.GetVariableNumeric("WixBundleElevated")));
531 }
532
533 private void Log(string format, params object[] args)
534 {
535 string relation = this.Command.Relation != RelationType.None ? String.Concat(" (", this.Command.Relation.ToString().ToLowerInvariant(), ")") : String.Empty;
536 string message = String.Format(format, args);
537
538 this.Engine.Log(LogLevel.Standard, String.Concat("TESTBA", relation, ": ", message));
539 }
540
541 private List<string> ReadVerifyArguments()
542 {
543 string testName = this.Engine.GetVariableString("TestGroupName");
544 using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}", testName)))
545 {
546 string verifyArguments = testKey == null ? null : testKey.GetValue("VerifyArguments") as string;
547 return verifyArguments == null ? new List<string>() : new List<string>(verifyArguments.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries));
548 }
549 }
550
551 private string ReadPackageAction(string packageId, string state)
552 {
553 string testName = this.Engine.GetVariableString("TestGroupName");
554 using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}\{1}", testName, String.IsNullOrEmpty(packageId) ? String.Empty : packageId)))
555 {
556 return testKey == null ? null : testKey.GetValue(state) as string;
557 }
558 }
559
560 private string ReadFeatureAction(string packageId, string featureId, string state)
561 {
562 string testName = this.Engine.GetVariableString("TestGroupName");
563 using (RegistryKey testKey = Registry.LocalMachine.OpenSubKey(String.Format(@"Software\WiX\Tests\TestBAControl\{0}\{1}", testName, packageId)))
564 {
565 string registryName = String.Concat(featureId, state);
566 return testKey == null ? null : testKey.GetValue(registryName) as string;
567 }
568 }
569
570 private static bool TryParseEnum<T>(string value, out T t)
571 {
572 try
573 {
574 t = (T)Enum.Parse(typeof(T), value, true);
575 return true;
576 }
577 catch (ArgumentException) { }
578 catch (OverflowException) { }
579
580 t = default(T);
581 return false;
582 }
583 }
584}
diff --git a/src/TestBA/TestBA.csproj b/src/TestBA/TestBA.csproj
new file mode 100644
index 00000000..b6aea84b
--- /dev/null
+++ b/src/TestBA/TestBA.csproj
@@ -0,0 +1,24 @@
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 <TargetFrameworks>net35;net5.0-windows</TargetFrameworks>
7 <AssemblyName>TestBA</AssemblyName>
8 <RootNamespace>WixToolset.Test.BA</RootNamespace>
9 <DebugType>embedded</DebugType>
10 <RuntimeIdentifier>win-x86</RuntimeIdentifier>
11 <EnableDynamicLoading>true</EnableDynamicLoading>
12 <UseWindowsForms>true</UseWindowsForms>
13 <RollForward>Major</RollForward>
14 </PropertyGroup>
15
16 <ItemGroup Condition=" '$(TargetFramework)'=='net35' ">
17 <Content Include="TestBA.BootstrapperCore.config" CopyToOutputDirectory="PreserveNewest" />
18 <Reference Include="System.Windows.Forms" />
19 </ItemGroup>
20
21 <ItemGroup>
22 <PackageReference Include="WixToolset.Mba.Core" Version="4.0.48" />
23 </ItemGroup>
24</Project> \ No newline at end of file
diff --git a/src/TestBA/TestBAFactory.cs b/src/TestBA/TestBAFactory.cs
new file mode 100644
index 00000000..ba1de367
--- /dev/null
+++ b/src/TestBA/TestBAFactory.cs
@@ -0,0 +1,22 @@
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}