aboutsummaryrefslogtreecommitdiff
path: root/src/internal
diff options
context:
space:
mode:
authorBob Arnson <bob@firegiant.com>2025-02-14 22:29:39 -0500
committerBob Arnson <github@bobs.org>2025-03-03 14:25:07 -0500
commitca6e44d496b0c589fdaabad69a00643f539c47cd (patch)
treeedf84727cecfc03092a2851b465d97622c5048eb /src/internal
parentba7fd5837ea149b2e319cc577fad27ce1162a064 (diff)
downloadwix-ca6e44d496b0c589fdaabad69a00643f539c47cd.tar.gz
wix-ca6e44d496b0c589fdaabad69a00643f539c47cd.tar.bz2
wix-ca6e44d496b0c589fdaabad69a00643f539c47cd.zip
Convert ext\ to MSTest and traversal projects.
- Move ext\ unit tests to MSTest. - MSBuildify ext projects with MSTest execution. - Fork test support projects for MSTest: - WixInternal.TestSupport - WixInternal.Core.TestPackage
Diffstat (limited to 'src/internal')
-rw-r--r--src/internal/SetBuildNumber/Directory.Packages.props.pp6
-rw-r--r--src/internal/SetBuildNumber/global.json.pp3
-rw-r--r--src/internal/WixInternal.MSTestSupport/Builder.cs204
-rw-r--r--src/internal/WixInternal.MSTestSupport/DisposableFileSystem.cs94
-rw-r--r--src/internal/WixInternal.MSTestSupport/DotnetRunner.cs57
-rw-r--r--src/internal/WixInternal.MSTestSupport/ExternalExecutable.cs374
-rw-r--r--src/internal/WixInternal.MSTestSupport/ExternalExecutableResult.cs19
-rw-r--r--src/internal/WixInternal.MSTestSupport/FakeBuildEngine.cs33
-rw-r--r--src/internal/WixInternal.MSTestSupport/MsbuildRunner.cs112
-rw-r--r--src/internal/WixInternal.MSTestSupport/MsbuildRunnerResult.cs19
-rw-r--r--src/internal/WixInternal.MSTestSupport/MsbuildUtilities.cs99
-rw-r--r--src/internal/WixInternal.MSTestSupport/Pushd.cs46
-rw-r--r--src/internal/WixInternal.MSTestSupport/Query.cs207
-rw-r--r--src/internal/WixInternal.MSTestSupport/RobocopyRunner.cs16
-rw-r--r--src/internal/WixInternal.MSTestSupport/TestData.cs78
-rw-r--r--src/internal/WixInternal.MSTestSupport/TestDataFolderFileSystem.cs42
-rw-r--r--src/internal/WixInternal.MSTestSupport/VswhereRunner.cs41
-rw-r--r--src/internal/WixInternal.MSTestSupport/WixAssert.cs164
-rw-r--r--src/internal/WixInternal.MSTestSupport/WixInternal.MSTestSupport.csproj27
-rw-r--r--src/internal/WixInternal.TestSupport/WixAssert.cs143
-rw-r--r--src/internal/WixInternal.TestSupport/XunitExtensions/WixAssert.cs26
-rw-r--r--src/internal/internal.sln36
-rw-r--r--src/internal/internal_t.proj1
23 files changed, 1846 insertions, 1 deletions
diff --git a/src/internal/SetBuildNumber/Directory.Packages.props.pp b/src/internal/SetBuildNumber/Directory.Packages.props.pp
index 8cf271d7..18a9b5eb 100644
--- a/src/internal/SetBuildNumber/Directory.Packages.props.pp
+++ b/src/internal/SetBuildNumber/Directory.Packages.props.pp
@@ -17,6 +17,7 @@
17 <PackageVersion Include="WixToolset.Dtf.WindowsInstaller.Package" Version="{packageversion}" /> 17 <PackageVersion Include="WixToolset.Dtf.WindowsInstaller.Package" Version="{packageversion}" />
18 18
19 <PackageVersion Include="WixInternal.TestSupport" Version="{packageversion}" /> 19 <PackageVersion Include="WixInternal.TestSupport" Version="{packageversion}" />
20 <PackageVersion Include="WixInternal.MSTestSupport" Version="{packageversion}" />
20 <PackageVersion Include="WixInternal.TestSupport.Native" Version="{packageversion}" /> 21 <PackageVersion Include="WixInternal.TestSupport.Native" Version="{packageversion}" />
21 <PackageVersion Include="WixInternal.BaseBuildTasks.Sources" Version="{packageversion}" /> 22 <PackageVersion Include="WixInternal.BaseBuildTasks.Sources" Version="{packageversion}" />
22 23
@@ -37,6 +38,7 @@
37 <PackageVersion Include="WixToolset.Core.Burn" Version="{packageversion}" /> 38 <PackageVersion Include="WixToolset.Core.Burn" Version="{packageversion}" />
38 <PackageVersion Include="WixToolset.Core.WindowsInstaller" Version="{packageversion}" /> 39 <PackageVersion Include="WixToolset.Core.WindowsInstaller" Version="{packageversion}" />
39 <PackageVersion Include="WixInternal.Core.TestPackage" Version="{packageversion}" /> 40 <PackageVersion Include="WixInternal.Core.TestPackage" Version="{packageversion}" />
41 <PackageVersion Include="WixInternal.Core.MSTestPackage" Version="{packageversion}" />
40 42
41 <PackageVersion Include="WixToolset.Heat" Version="{packageversion}" /> 43 <PackageVersion Include="WixToolset.Heat" Version="{packageversion}" />
42 44
@@ -98,6 +100,10 @@
98 </ItemGroup> 100 </ItemGroup>
99 101
100 <ItemGroup> 102 <ItemGroup>
103 <PackageVersion Include="MSTest.TestFramework" Version="3.7.3" />
104 </ItemGroup>
105
106 <ItemGroup>
101 <PackageVersion Include="Microsoft.NET.Tools.NETCoreCheck.x86" Version="6.0.0" /> 107 <PackageVersion Include="Microsoft.NET.Tools.NETCoreCheck.x86" Version="6.0.0" />
102 <PackageVersion Include="Microsoft.NET.Tools.NETCoreCheck.x64" Version="6.0.0" /> 108 <PackageVersion Include="Microsoft.NET.Tools.NETCoreCheck.x64" Version="6.0.0" />
103 <PackageVersion Include="Microsoft.NET.Tools.NETCoreCheck.arm64" Version="6.0.0" /> 109 <PackageVersion Include="Microsoft.NET.Tools.NETCoreCheck.arm64" Version="6.0.0" />
diff --git a/src/internal/SetBuildNumber/global.json.pp b/src/internal/SetBuildNumber/global.json.pp
index bad3c0bd..fe48e6cc 100644
--- a/src/internal/SetBuildNumber/global.json.pp
+++ b/src/internal/SetBuildNumber/global.json.pp
@@ -1,6 +1,7 @@
1{ 1{
2 "msbuild-sdks": { 2 "msbuild-sdks": {
3 "Microsoft.Build.Traversal": "3.2.0", 3 "MSTest.Sdk": "3.8.0",
4 "Microsoft.Build.Traversal": "4.1.82",
4 "Microsoft.Build.NoTargets": "3.5.6", 5 "Microsoft.Build.NoTargets": "3.5.6",
5 "WixToolset.Sdk": "{packageversion}" 6 "WixToolset.Sdk": "{packageversion}"
6 }, 7 },
diff --git a/src/internal/WixInternal.MSTestSupport/Builder.cs b/src/internal/WixInternal.MSTestSupport/Builder.cs
new file mode 100644
index 00000000..62f2891b
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/Builder.cs
@@ -0,0 +1,204 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8
9 public class Builder
10 {
11 public Builder(string sourceFolder, Type extensionType = null, string[] bindPaths = null, string outputFile = null)
12 {
13 this.SourceFolder = sourceFolder;
14 if (extensionType != null)
15 {
16 this.ExtensionTypes = new Type[] { extensionType };
17 }
18 else
19 {
20 this.ExtensionTypes = new Type[] { };
21 }
22 this.BindPaths = bindPaths;
23 this.OutputFile = outputFile ?? "test.msi";
24 }
25
26 public Builder(string sourceFolder, Type[] extensionTypes, string[] bindPaths = null, string outputFile = null)
27 {
28 this.SourceFolder = sourceFolder;
29 this.ExtensionTypes = extensionTypes;
30 this.BindPaths = bindPaths;
31 this.OutputFile = outputFile ?? "test.msi";
32 }
33
34 public string[] BindPaths { get; set; }
35
36 public Type[] ExtensionTypes { get; set; }
37
38 public string OutputFile { get; set; }
39
40 public string SourceFolder { get; }
41
42 public string[] BuildAndQuery(Action<string[]> buildFunc, params string[] tables)
43 {
44 return this.BuildAndQuery(buildFunc, validate: false, tables);
45 }
46
47 public string[] BuildAndQuery(Action<string[]> buildFunc, bool validate, params string[] tables)
48 {
49 var sourceFiles = Directory.GetFiles(this.SourceFolder, "*.wxs");
50 var wxlFiles = Directory.GetFiles(this.SourceFolder, "*.wxl");
51
52 using (var fs = new DisposableFileSystem())
53 {
54 var intermediateFolder = fs.GetFolder();
55 var outputPath = Path.Combine(intermediateFolder, "bin", this.OutputFile);
56
57 var args = new List<string>
58 {
59 "build",
60 "-o", outputPath,
61 "-intermediateFolder", intermediateFolder,
62 };
63
64 foreach (var ext in this.ExtensionTypes)
65 {
66 args.Add("-ext");
67 args.Add(Path.GetFullPath(ext.Assembly.Location));
68 }
69
70 args.AddRange(sourceFiles);
71
72 foreach (var wxlFile in wxlFiles)
73 {
74 args.Add("-loc");
75 args.Add(wxlFile);
76 }
77
78 foreach (var bindPath in this.BindPaths)
79 {
80 args.Add("-bindpath");
81 args.Add(bindPath);
82 }
83
84 buildFunc(args.ToArray());
85
86 if (validate)
87 {
88 args = new List<string>
89 {
90 "msi",
91 "validate",
92 "-intermediateFolder", intermediateFolder,
93 outputPath,
94 };
95
96 buildFunc(args.ToArray());
97 }
98
99 return Query.QueryDatabase(outputPath, tables);
100 }
101 }
102
103 public void BuildAndDecompileAndBuild(Action<string[]> buildFunc, Action<string[]> decompileFunc, string decompilePath, bool validate = false)
104 {
105 var sourceFiles = Directory.GetFiles(this.SourceFolder, "*.wxs");
106 var wxlFiles = Directory.GetFiles(this.SourceFolder, "*.wxl");
107
108 using (var fs = new DisposableFileSystem())
109 {
110 var intermediateFolder = fs.GetFolder();
111 var outputFolder = Path.Combine(intermediateFolder, "bin");
112 var decompileExtractFolder = Path.Combine(intermediateFolder, "decompiled", "extract");
113 var decompileIntermediateFolder = Path.Combine(intermediateFolder, "decompiled", "obj");
114 var decompileBuildFolder = Path.Combine(intermediateFolder, "decompiled", "bin");
115 var outputPath = Path.Combine(outputFolder, this.OutputFile);
116 var decompileBuildPath = Path.Combine(decompileBuildFolder, this.OutputFile);
117
118 // First build.
119 var firstBuildArgs = new List<string>
120 {
121 "build",
122 "-o", outputPath,
123 "-intermediateFolder", intermediateFolder,
124 };
125
126 foreach (var ext in this.ExtensionTypes)
127 {
128 firstBuildArgs.Add("-ext");
129 firstBuildArgs.Add(Path.GetFullPath(ext.Assembly.Location));
130 }
131
132 firstBuildArgs.AddRange(sourceFiles);
133
134 foreach (var wxlFile in wxlFiles)
135 {
136 firstBuildArgs.Add("-loc");
137 firstBuildArgs.Add(wxlFile);
138 }
139
140 foreach (var bindPath in this.BindPaths)
141 {
142 firstBuildArgs.Add("-bindpath");
143 firstBuildArgs.Add(bindPath);
144 }
145
146 buildFunc(firstBuildArgs.ToArray());
147
148 if (validate)
149 {
150 firstBuildArgs = new List<string>
151 {
152 "msi",
153 "validate",
154 "-intermediateFolder", intermediateFolder,
155 outputPath,
156 };
157
158 buildFunc(firstBuildArgs.ToArray());
159 }
160
161 // Decompile built output.
162 var decompileArgs = new List<string>
163 {
164 "msi", "decompile",
165 outputPath,
166 "-intermediateFolder", decompileIntermediateFolder,
167 "-x", decompileExtractFolder,
168 "-o", decompilePath
169 };
170
171 foreach (var ext in this.ExtensionTypes)
172 {
173 decompileArgs.Add("-ext");
174 decompileArgs.Add(Path.GetFullPath(ext.Assembly.Location));
175 }
176
177 decompileFunc(decompileArgs.ToArray());
178
179 // Build decompiled output.
180 var secondBuildArgs = new List<string>
181 {
182 "build",
183 decompilePath,
184 "-o", decompileBuildPath,
185 "-intermediateFolder", decompileIntermediateFolder
186 };
187
188 foreach (var ext in this.ExtensionTypes)
189 {
190 secondBuildArgs.Add("-ext");
191 secondBuildArgs.Add(Path.GetFullPath(ext.Assembly.Location));
192 }
193
194 secondBuildArgs.Add("-bindpath");
195 secondBuildArgs.Add(outputFolder);
196
197 secondBuildArgs.Add("-bindpath");
198 secondBuildArgs.Add(decompileExtractFolder);
199
200 buildFunc(secondBuildArgs.ToArray());
201 }
202 }
203 }
204}
diff --git a/src/internal/WixInternal.MSTestSupport/DisposableFileSystem.cs b/src/internal/WixInternal.MSTestSupport/DisposableFileSystem.cs
new file mode 100644
index 00000000..5e4bb23f
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/DisposableFileSystem.cs
@@ -0,0 +1,94 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8
9 public class DisposableFileSystem : IDisposable
10 {
11 protected bool Disposed { get; private set; }
12
13 private List<string> CleanupPaths { get; } = new List<string>();
14
15 public bool Keep { get; }
16
17 public DisposableFileSystem(bool keep = false)
18 {
19 this.Keep = keep;
20 }
21
22 protected string GetFile(bool create = false)
23 {
24 var path = Path.GetTempFileName();
25
26 if (!create)
27 {
28 File.Delete(path);
29 }
30
31 this.CleanupPaths.Add(path);
32
33 return path;
34 }
35
36 public string GetFolder(bool create = false)
37 {
38 // Always return a path with a space in it.
39 var path = Path.Combine(Path.GetTempPath(), ".WIXTEST " + Path.GetRandomFileName());
40
41 if (create)
42 {
43 Directory.CreateDirectory(path);
44 }
45
46 this.CleanupPaths.Add(path);
47
48 return path;
49 }
50
51
52 #region // IDisposable
53
54 public void Dispose()
55 {
56 this.Dispose(true);
57 GC.SuppressFinalize(this);
58 }
59
60 protected virtual void Dispose(bool disposing)
61 {
62 if (this.Disposed)
63 {
64 return;
65 }
66
67 if (disposing && !this.Keep)
68 {
69 foreach (var path in this.CleanupPaths)
70 {
71 try
72 {
73 if (File.Exists(path))
74 {
75 File.Delete(path);
76 }
77 else if (Directory.Exists(path))
78 {
79 Directory.Delete(path, true);
80 }
81 }
82 catch
83 {
84 // Best effort delete, so ignore any failures.
85 }
86 }
87 }
88
89 this.Disposed = true;
90 }
91
92 #endregion
93 }
94}
diff --git a/src/internal/WixInternal.MSTestSupport/DotnetRunner.cs b/src/internal/WixInternal.MSTestSupport/DotnetRunner.cs
new file mode 100644
index 00000000..8fa3a739
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/DotnetRunner.cs
@@ -0,0 +1,57 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8
9 public class DotnetRunner : ExternalExecutable
10 {
11 private static readonly object InitLock = new object();
12 private static bool Initialized;
13 private static DotnetRunner Instance;
14
15 public static ExternalExecutableResult Execute(string command, string[] arguments = null) =>
16 InitAndExecute(command, arguments);
17
18 private static ExternalExecutableResult InitAndExecute(string command, string[] arguments)
19 {
20 lock (InitLock)
21 {
22 if (!Initialized)
23 {
24 Initialized = true;
25 var dotnetPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
26 if (String.IsNullOrEmpty(dotnetPath) || !File.Exists(dotnetPath))
27 {
28 dotnetPath = "dotnet";
29 }
30
31 Instance = new DotnetRunner(dotnetPath);
32 }
33 }
34
35 return Instance.ExecuteCore(command, arguments);
36 }
37
38 private DotnetRunner(string exePath) : base(exePath) { }
39
40 private ExternalExecutableResult ExecuteCore(string command, string[] arguments)
41 {
42 var total = new List<string>
43 {
44 command,
45 };
46
47 if (arguments != null)
48 {
49 total.AddRange(arguments);
50 }
51
52 var args = CombineArguments(total);
53 var mergeErrorIntoOutput = true;
54 return this.Run(args, mergeErrorIntoOutput);
55 }
56 }
57}
diff --git a/src/internal/WixInternal.MSTestSupport/ExternalExecutable.cs b/src/internal/WixInternal.MSTestSupport/ExternalExecutable.cs
new file mode 100644
index 00000000..927240ec
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/ExternalExecutable.cs
@@ -0,0 +1,374 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6 using System.Collections.Concurrent;
7 using System.Collections.Generic;
8 using System.ComponentModel;
9 using System.Diagnostics;
10 using System.IO;
11 using System.Runtime.InteropServices;
12 using System.Text;
13 using System.Threading.Tasks;
14 using Microsoft.Win32.SafeHandles;
15
16 public abstract class ExternalExecutable
17 {
18 private readonly string exePath;
19
20 protected ExternalExecutable(string exePath)
21 {
22 this.exePath = exePath;
23 }
24
25 protected ExternalExecutableResult Run(string args, bool mergeErrorIntoOutput = false, string workingDirectory = null)
26 {
27 // https://github.com/dotnet/runtime/issues/58492
28 // Process.Start doesn't currently support starting a process with a long path,
29 // but the way to support long paths doesn't support searching for the executable if it was a relative path.
30 // Avoid the managed way of doing this even if the target isn't a long path to help verify that the native way works.
31 if (!Path.IsPathRooted(this.exePath))
32 {
33 return this.RunManaged(args, mergeErrorIntoOutput, workingDirectory);
34 }
35
36 // https://web.archive.org/web/20150331190801/https://support.microsoft.com/en-us/kb/190351
37 var commandLine = $"\"{this.exePath}\" {args}";
38 var currentDirectory = workingDirectory ?? Path.GetDirectoryName(this.exePath);
39 if (String.IsNullOrEmpty(currentDirectory))
40 {
41 currentDirectory = null;
42 }
43 var processInfo = new PROCESS_INFORMATION();
44 var startInfo = new STARTUPINFOW
45 {
46 cb = Marshal.SizeOf(typeof(STARTUPINFOW)),
47 dwFlags = StartupInfoFlags.STARTF_FORCEOFFFEEDBACK | StartupInfoFlags.STARTF_USESTDHANDLES,
48 hStdInput = GetStdHandle(StdHandleType.STD_INPUT_HANDLE),
49 };
50 SafeFileHandle hStdOutputParent = null;
51 SafeFileHandle hStdErrorParent = null;
52
53 try
54 {
55 CreatePipeForProcess(out hStdOutputParent, out startInfo.hStdOutput);
56
57 if (!mergeErrorIntoOutput)
58 {
59 CreatePipeForProcess(out hStdErrorParent, out startInfo.hStdError);
60 }
61 else
62 {
63 if (!DuplicateHandle(GetCurrentProcess(), startInfo.hStdOutput, GetCurrentProcess(), out startInfo.hStdError, 0, true, DuplicateHandleOptions.DUPLICATE_SAME_ACCESS))
64 {
65 throw new Win32Exception();
66 }
67 }
68
69 if (!CreateProcessW(this.exePath, commandLine, IntPtr.Zero, IntPtr.Zero, true, CreateProcessFlags.CREATE_NO_WINDOW, IntPtr.Zero,
70 currentDirectory, ref startInfo, ref processInfo))
71 {
72 throw new Win32Exception();
73 }
74
75 startInfo.Dispose();
76
77 return GetResultFromNative(mergeErrorIntoOutput, hStdOutputParent, hStdErrorParent, processInfo.hProcess, this.exePath, args);
78 }
79 finally
80 {
81 hStdErrorParent?.Dispose();
82 hStdOutputParent?.Dispose();
83
84 startInfo.Dispose();
85 processInfo.Dispose();
86 }
87 }
88
89 private static ExternalExecutableResult GetResultFromNative(bool mergeErrorIntoOutput, SafeFileHandle hStdOutputParent, SafeFileHandle hStdErrorParent, IntPtr hProcess, string fileName, string args)
90 {
91 using (var outputStream = new StreamReader(new FileStream(hStdOutputParent, FileAccess.Read)))
92 using (var errorStream = mergeErrorIntoOutput ? null : new StreamReader(new FileStream(hStdErrorParent, FileAccess.Read)))
93 {
94 var outputTask = Task.Run(() => ReadProcessStreamLines(outputStream));
95 var errorTask = Task.Run(() => ReadProcessStreamLines(errorStream));
96
97 while (!outputTask.Wait(100) || !errorTask.Wait(100)) { Task.Yield(); }
98 var standardOutput = outputTask.Result;
99 var standardError = errorTask.Result;
100
101 if (WaitForSingleObject(hProcess, -1) != 0)
102 {
103 throw new Win32Exception();
104 }
105
106 if (!GetExitCodeProcess(hProcess, out var exitCode))
107 {
108 throw new Win32Exception();
109 }
110
111 return new ExternalExecutableResult
112 {
113 ExitCode = exitCode,
114 StandardError = standardError,
115 StandardOutput = standardOutput,
116 FileName = fileName,
117 Arguments = args,
118 };
119 }
120 }
121
122 private static string[] ReadProcessStreamLines(StreamReader streamReader)
123 {
124 if (streamReader == null)
125 {
126 return null;
127 }
128
129 var lines = new List<string>();
130 while (true)
131 {
132 var line = streamReader.ReadLine();
133 if (line == null)
134 {
135 break;
136 }
137
138 lines.Add(line);
139 }
140
141 return lines.ToArray();
142 }
143
144 protected ExternalExecutableResult RunManaged(string args, bool mergeErrorIntoOutput = false, string workingDirectory = null)
145 {
146 var startInfo = new ProcessStartInfo(this.exePath, args)
147 {
148 CreateNoWindow = true,
149 RedirectStandardError = true,
150 RedirectStandardOutput = true,
151 UseShellExecute = false,
152 WorkingDirectory = workingDirectory ?? Path.GetDirectoryName(this.exePath),
153 };
154
155 using (var process = Process.Start(startInfo))
156 {
157 // This implementation of merging the streams does not guarantee that lines are retrieved in the same order that they were written.
158 // If the process is simultaneously writing to both streams, this is impossible to do anyway.
159 var standardOutput = new ConcurrentQueue<string>();
160 var standardError = mergeErrorIntoOutput ? standardOutput : new ConcurrentQueue<string>();
161
162 process.ErrorDataReceived += (s, e) => { if (e.Data != null) { standardError.Enqueue(e.Data); } };
163 process.OutputDataReceived += (s, e) => { if (e.Data != null) { standardOutput.Enqueue(e.Data); } };
164
165 process.BeginErrorReadLine();
166 process.BeginOutputReadLine();
167
168 process.WaitForExit();
169
170 return new ExternalExecutableResult
171 {
172 ExitCode = process.ExitCode,
173 StandardError = mergeErrorIntoOutput ? null : standardError.ToArray(),
174 StandardOutput = standardOutput.ToArray(),
175 FileName = this.exePath,
176 Arguments = args,
177 };
178 }
179 }
180
181 // This is internal because it assumes backslashes aren't used as escape characters and there aren't any double quotes.
182 internal static string CombineArguments(IEnumerable<string> arguments)
183 {
184 if (arguments == null)
185 {
186 return null;
187 }
188
189 var sb = new StringBuilder();
190
191 foreach (var arg in arguments)
192 {
193 if (sb.Length > 0)
194 {
195 sb.Append(' ');
196 }
197
198 if (arg.IndexOf(' ') > -1 && !arg.EndsWith("\""))
199 {
200 sb.Append("\"");
201 sb.Append(arg);
202 sb.Append("\"");
203 }
204 else
205 {
206 sb.Append(arg);
207 }
208 }
209
210 return sb.ToString();
211 }
212
213 private static void CreatePipeForProcess(out SafeFileHandle hReadPipe, out IntPtr hWritePipe)
214 {
215 var securityAttributes = new SECURITY_ATTRIBUTES
216 {
217 nLength = Marshal.SizeOf(typeof(SECURITY_ATTRIBUTES)),
218 bInheritHandle = true,
219 };
220
221 if (!CreatePipe(out var hReadTemp, out hWritePipe, ref securityAttributes, 0))
222 {
223 throw new Win32Exception();
224 }
225
226 // Only the handle passed to the process should be inheritable, so have to duplicate the other handle to get an uninheritable one.
227 if (!DuplicateHandle(GetCurrentProcess(), hReadTemp, GetCurrentProcess(), out var hReadPipePtr, 0, false, DuplicateHandleOptions.DUPLICATE_CLOSE_SOURCE | DuplicateHandleOptions.DUPLICATE_SAME_ACCESS))
228 {
229 throw new Win32Exception();
230 }
231
232 hReadPipe = new SafeFileHandle(hReadPipePtr, true);
233 }
234
235 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
236 private extern static IntPtr GetStdHandle(StdHandleType nStdHandle);
237
238 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
239 [return: MarshalAs(UnmanagedType.Bool)]
240 private extern static bool CreatePipe(out IntPtr hReadPipe, out IntPtr hWritePipe, ref SECURITY_ATTRIBUTES lpPipeAttributes, int nSize);
241
242 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
243 [return: MarshalAs(UnmanagedType.Bool)]
244 private extern static bool CreateProcessW(
245 string lpApplicationName,
246 string lpCommandLine,
247 IntPtr lpProcessAttributes,
248 IntPtr lpThreadAttributes,
249 [MarshalAs(UnmanagedType.Bool)] bool bInheritHandles,
250 CreateProcessFlags dwCreationFlags,
251 IntPtr lpEnvironment,
252 string lpCurrentDirectory,
253 ref STARTUPINFOW lpStartupInfo,
254 ref PROCESS_INFORMATION lpProcessInformation);
255
256 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
257 private extern static IntPtr GetCurrentProcess();
258
259 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
260 [return: MarshalAs(UnmanagedType.Bool)]
261 private extern static bool GetExitCodeProcess(IntPtr hHandle, out int lpExitCode);
262
263 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
264 private extern static int WaitForSingleObject(IntPtr hHandle, int dwMilliseconds);
265
266 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
267 [return: MarshalAs(UnmanagedType.Bool)]
268 private extern static bool CloseHandle(IntPtr hObject);
269
270 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
271 [return: MarshalAs(UnmanagedType.Bool)]
272 private extern static bool DuplicateHandle(IntPtr hSourceProcessHandle, IntPtr hSourceHandle, IntPtr hTargetProcessHandle, out IntPtr lpTargetHandle, int dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, DuplicateHandleOptions dwOptions);
273
274 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
275 private struct SECURITY_ATTRIBUTES
276 {
277 public int nLength;
278 public IntPtr lpSecurityDescriptor;
279 [MarshalAs(UnmanagedType.Bool)]
280 public bool bInheritHandle;
281 }
282
283 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
284 private struct STARTUPINFOW
285 {
286 public int cb;
287 public string lpReserved;
288 public string lpDesktop;
289 public string lpTitle;
290 public int dwX;
291 public int dwY;
292 public int dwXSize;
293 public int dwYSize;
294 public int dwXCountChars;
295 public int dwYCountChars;
296 public int dwFillAttribute;
297 public StartupInfoFlags dwFlags;
298 public short wShowWindow;
299 public short cbReserved2;
300 public IntPtr lpReserved2;
301 public IntPtr hStdInput;
302 public IntPtr hStdOutput;
303 public IntPtr hStdError;
304
305 public void Dispose()
306 {
307 // This makes assumptions based on how it's used above.
308 if (this.hStdError != IntPtr.Zero)
309 {
310 CloseHandle(this.hStdError);
311 this.hStdError = IntPtr.Zero;
312 }
313
314 if (this.hStdOutput != IntPtr.Zero)
315 {
316 CloseHandle(this.hStdOutput);
317 this.hStdOutput = IntPtr.Zero;
318 }
319 }
320 }
321
322 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
323 private struct PROCESS_INFORMATION
324 {
325 public IntPtr hProcess;
326 public IntPtr hThread;
327 public int dwProcessId;
328 public int dwThreadId;
329
330 public void Dispose()
331 {
332 if (this.hProcess != IntPtr.Zero)
333 {
334 CloseHandle(this.hProcess);
335 this.hProcess = IntPtr.Zero;
336 }
337
338 if (this.hThread != IntPtr.Zero)
339 {
340 CloseHandle(this.hThread);
341 this.hThread = IntPtr.Zero;
342 }
343 }
344 }
345
346 private enum StdHandleType
347 {
348 STD_INPUT_HANDLE = -10,
349 STD_OUTPUT_HANDLE = -11,
350 STD_ERROR_HANDLE = -12,
351 }
352
353 [Flags]
354 private enum CreateProcessFlags
355 {
356 None = 0x0,
357 CREATE_NO_WINDOW = 0x08000000,
358 }
359
360 [Flags]
361 private enum StartupInfoFlags
362 {
363 None = 0x0,
364 STARTF_FORCEOFFFEEDBACK = 0x80,
365 STARTF_USESTDHANDLES = 0x100,
366 }
367
368 private enum DuplicateHandleOptions
369 {
370 DUPLICATE_CLOSE_SOURCE = 1,
371 DUPLICATE_SAME_ACCESS = 2,
372 }
373 }
374}
diff --git a/src/internal/WixInternal.MSTestSupport/ExternalExecutableResult.cs b/src/internal/WixInternal.MSTestSupport/ExternalExecutableResult.cs
new file mode 100644
index 00000000..57bf0d11
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/ExternalExecutableResult.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 WixInternal.MSTestSupport
4{
5 using System.Diagnostics;
6
7 public class ExternalExecutableResult
8 {
9 public int ExitCode { get; set; }
10
11 public string[] StandardError { get; set; }
12
13 public string[] StandardOutput { get; set; }
14
15 public string FileName { get; set; }
16
17 public string Arguments { get; set; }
18 }
19}
diff --git a/src/internal/WixInternal.MSTestSupport/FakeBuildEngine.cs b/src/internal/WixInternal.MSTestSupport/FakeBuildEngine.cs
new file mode 100644
index 00000000..951d1bc8
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/FakeBuildEngine.cs
@@ -0,0 +1,33 @@
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 WixInternal.MSTestSupport
4{
5 using System.Collections;
6 using System.Text;
7 using Microsoft.Build.Framework;
8
9 public class FakeBuildEngine : IBuildEngine
10 {
11 private readonly StringBuilder output = new StringBuilder();
12
13 public int ColumnNumberOfTaskNode => 0;
14
15 public bool ContinueOnError => false;
16
17 public int LineNumberOfTaskNode => 0;
18
19 public string ProjectFileOfTaskNode => "fake_wix.targets";
20
21 public string Output => this.output.ToString();
22
23 public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => throw new System.NotImplementedException();
24
25 public void LogCustomEvent(CustomBuildEventArgs e) => this.output.AppendLine(e.Message);
26
27 public void LogErrorEvent(BuildErrorEventArgs e) => this.output.AppendLine(e.Message);
28
29 public void LogMessageEvent(BuildMessageEventArgs e) => this.output.AppendLine(e.Message);
30
31 public void LogWarningEvent(BuildWarningEventArgs e) => this.output.AppendLine(e.Message);
32 }
33}
diff --git a/src/internal/WixInternal.MSTestSupport/MsbuildRunner.cs b/src/internal/WixInternal.MSTestSupport/MsbuildRunner.cs
new file mode 100644
index 00000000..69fc7292
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/MsbuildRunner.cs
@@ -0,0 +1,112 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8
9 public class MsbuildRunner : ExternalExecutable
10 {
11 private static readonly string VswhereFindArguments = "-property installationPath -version [17.0,18.0)";
12 private static readonly string MsbuildCurrentRelativePath = @"MSBuild\Current\Bin\MSBuild.exe";
13 private static readonly string MsbuildCurrentRelativePath64 = @"MSBuild\Current\Bin\amd64\MSBuild.exe";
14
15 private static readonly object InitLock = new object();
16
17 private static bool Initialized;
18 private static MsbuildRunner MsbuildCurrentRunner;
19 private static MsbuildRunner MsbuildCurrentRunner64;
20
21 public static MsbuildRunnerResult Execute(string projectPath, string[] arguments = null, bool x64 = false) =>
22 InitAndExecute(String.Empty, projectPath, arguments, x64);
23
24 public static MsbuildRunnerResult ExecuteWithMsbuildCurrent(string projectPath, string[] arguments = null, bool x64 = false) =>
25 InitAndExecute("Current", projectPath, arguments, x64);
26
27 private static MsbuildRunnerResult InitAndExecute(string msbuildVersion, string projectPath, string[] arguments, bool x64)
28 {
29 lock (InitLock)
30 {
31 if (!Initialized)
32 {
33 Initialized = true;
34 var vswhereResult = VswhereRunner.Execute(VswhereFindArguments, true);
35 if (vswhereResult.ExitCode != 0)
36 {
37 throw new InvalidOperationException($"Failed to execute vswhere.exe, exit code: {vswhereResult.ExitCode}. Output:\r\n{String.Join("\r\n", vswhereResult.StandardOutput)}");
38 }
39
40 string msbuildCurrentPath = null;
41 string msbuildCurrentPath64 = null;
42
43 foreach (var installPath in vswhereResult.StandardOutput)
44 {
45 if (msbuildCurrentPath == null)
46 {
47 var path = Path.Combine(installPath, MsbuildCurrentRelativePath);
48 if (File.Exists(path))
49 {
50 msbuildCurrentPath = path;
51 }
52 }
53
54 if (msbuildCurrentPath64 == null)
55 {
56 var path = Path.Combine(installPath, MsbuildCurrentRelativePath64);
57 if (File.Exists(path))
58 {
59 msbuildCurrentPath64 = path;
60 }
61 }
62 }
63
64 if (msbuildCurrentPath != null)
65 {
66 MsbuildCurrentRunner = new MsbuildRunner(msbuildCurrentPath);
67 }
68
69 if (msbuildCurrentPath64 != null)
70 {
71 MsbuildCurrentRunner64 = new MsbuildRunner(msbuildCurrentPath64);
72 }
73 }
74 }
75
76 MsbuildRunner runner = x64 ? MsbuildCurrentRunner64 : MsbuildCurrentRunner;
77
78 if (runner == null)
79 {
80 throw new InvalidOperationException($"Failed to find an installed{(x64 ? " 64-bit" : String.Empty)} MSBuild{msbuildVersion}");
81 }
82
83 return runner.ExecuteCore(projectPath, arguments);
84 }
85
86 private MsbuildRunner(string exePath) : base(exePath) { }
87
88 private MsbuildRunnerResult ExecuteCore(string projectPath, string[] arguments)
89 {
90 var total = new List<string>
91 {
92 projectPath,
93 };
94
95 if (arguments != null)
96 {
97 total.AddRange(arguments);
98 }
99
100 var args = CombineArguments(total);
101 var mergeErrorIntoOutput = true;
102 var workingFolder = Path.GetDirectoryName(projectPath);
103 var result = this.Run(args, mergeErrorIntoOutput, workingFolder);
104
105 return new MsbuildRunnerResult
106 {
107 ExitCode = result.ExitCode,
108 Output = result.StandardOutput,
109 };
110 }
111 }
112}
diff --git a/src/internal/WixInternal.MSTestSupport/MsbuildRunnerResult.cs b/src/internal/WixInternal.MSTestSupport/MsbuildRunnerResult.cs
new file mode 100644
index 00000000..02e25ebb
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/MsbuildRunnerResult.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 WixInternal.MSTestSupport
4{
5 using System;
6 using Microsoft.VisualStudio.TestTools.UnitTesting;
7
8 public class MsbuildRunnerResult
9 {
10 public int ExitCode { get; set; }
11
12 public string[] Output { get; set; }
13
14 public void AssertSuccess()
15 {
16 Assert.IsTrue(0 == this.ExitCode, $"MSBuild failed unexpectedly. Output:{Environment.NewLine}{String.Join(Environment.NewLine, this.Output)}");
17 }
18 }
19}
diff --git a/src/internal/WixInternal.MSTestSupport/MsbuildUtilities.cs b/src/internal/WixInternal.MSTestSupport/MsbuildUtilities.cs
new file mode 100644
index 00000000..4776e6f1
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/MsbuildUtilities.cs
@@ -0,0 +1,99 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9
10 public enum BuildSystem
11 {
12 DotNetCoreSdk,
13 MSBuild,
14 MSBuild64,
15 }
16
17 public static class MsbuildUtilities
18 {
19 public static MsbuildRunnerResult BuildProject(BuildSystem buildSystem, string projectPath, string[] arguments = null, string configuration = "Release", string verbosityLevel = "normal", bool suppressValidation = true)
20 {
21 var allArgs = new List<string>
22 {
23 $"-verbosity:{verbosityLevel}",
24 $"-p:Configuration={configuration}",
25 $"-p:SuppressValidation={suppressValidation}",
26 // Node reuse means that child msbuild processes can stay around after the build completes.
27 // Under that scenario, the root msbuild does not reliably close its streams which causes us to hang.
28 "-nr:false",
29 MsbuildUtilities.GetQuotedSwitch(buildSystem, "bl", Path.ChangeExtension(projectPath, ".binlog"))
30 };
31
32 if (arguments != null)
33 {
34 allArgs.AddRange(arguments);
35 }
36
37 switch (buildSystem)
38 {
39 case BuildSystem.DotNetCoreSdk:
40 {
41 allArgs.Add(projectPath);
42 var result = DotnetRunner.Execute("msbuild", allArgs.ToArray());
43 return new MsbuildRunnerResult
44 {
45 ExitCode = result.ExitCode,
46 Output = result.StandardOutput,
47 };
48 }
49 case BuildSystem.MSBuild:
50 case BuildSystem.MSBuild64:
51 {
52 return MsbuildRunner.Execute(projectPath, allArgs.ToArray(), buildSystem == BuildSystem.MSBuild64);
53 }
54 default:
55 {
56 throw new NotImplementedException();
57 }
58 }
59 }
60
61 public static string GetQuotedSwitch(BuildSystem _, string switchName, string switchValue)
62 {
63 // If the value ends with a backslash, escape it.
64 if (switchValue?.EndsWith("\\") == true)
65 {
66 switchValue += @"\";
67 }
68
69 return $"-{switchName}:\"{switchValue}\"";
70 }
71
72 public static string GetQuotedPropertySwitch(BuildSystem buildSystem, string propertyName, string propertyValue)
73 {
74 // If the value ends with a backslash, escape it.
75 if (propertyValue?.EndsWith("\\") == true)
76 {
77 propertyValue += @"\";
78 }
79
80 var quotedValue = "\"" + propertyValue + "\"";
81
82 // If the value contains a semicolon then escape-quote it (wrap with the characters: \") to wrap the value
83 // instead of just quoting the value, otherwise dotnet.exe will not pass the value to MSBuild correctly.
84 if (buildSystem == BuildSystem.DotNetCoreSdk && propertyValue?.IndexOf(';') > -1)
85 {
86 quotedValue = "\\\"" + propertyValue + "\\\"";
87 }
88
89 return $"-p:{propertyName}={quotedValue}";
90 }
91
92 public static IEnumerable<string> GetToolCommandLines(MsbuildRunnerResult result, string toolName, string operation, BuildSystem buildSystem)
93 {
94 var expectedToolExe = buildSystem == BuildSystem.DotNetCoreSdk ? $"{toolName}.dll\"" : $"{toolName}.exe";
95 var expectedToolCommand = $"{expectedToolExe} {operation}";
96 return result.Output.Where(line => line.Contains(expectedToolCommand));
97 }
98 }
99}
diff --git a/src/internal/WixInternal.MSTestSupport/Pushd.cs b/src/internal/WixInternal.MSTestSupport/Pushd.cs
new file mode 100644
index 00000000..7086ffd0
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/Pushd.cs
@@ -0,0 +1,46 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6 using System.IO;
7
8 public class Pushd : IDisposable
9 {
10 protected bool Disposed { get; private set; }
11
12 public Pushd(string path)
13 {
14 this.PreviousDirectory = Directory.GetCurrentDirectory();
15
16 Directory.SetCurrentDirectory(path);
17 }
18
19 public string PreviousDirectory { get; }
20
21 #region // IDisposable
22
23 public void Dispose()
24 {
25 this.Dispose(true);
26 GC.SuppressFinalize(this);
27 }
28
29 protected virtual void Dispose(bool disposing)
30 {
31 if (this.Disposed)
32 {
33 return;
34 }
35
36 if (disposing)
37 {
38 Directory.SetCurrentDirectory(this.PreviousDirectory);
39 }
40
41 this.Disposed = true;
42 }
43
44 #endregion
45 }
46}
diff --git a/src/internal/WixInternal.MSTestSupport/Query.cs b/src/internal/WixInternal.MSTestSupport/Query.cs
new file mode 100644
index 00000000..5a8868b3
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/Query.cs
@@ -0,0 +1,207 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using System.Text;
10 using WixToolset.Dtf.Compression.Cab;
11 using WixToolset.Dtf.WindowsInstaller;
12
13 public class Query
14 {
15 public static string[] QueryDatabase(string path, string[] tables)
16 {
17 var results = new List<string>();
18 var resultsByTable = QueryDatabaseByTable(path, tables);
19 var sortedTables = tables.ToList();
20 sortedTables.Sort();
21 foreach (var tableName in sortedTables)
22 {
23 var rows = resultsByTable[tableName];
24 rows?.ForEach(r => results.Add($"{tableName}:{r}"));
25 }
26 return results.ToArray();
27 }
28
29 public static string[] QueryDatabase(Database db, string[] tables)
30 {
31 var results = new List<string>();
32 var resultsByTable = QueryDatabaseByTable(db, tables);
33 var sortedTables = tables.ToList();
34 sortedTables.Sort();
35 foreach (var tableName in sortedTables)
36 {
37 var rows = resultsByTable[tableName];
38 rows?.ForEach(r => results.Add($"{tableName}:{r}"));
39 }
40 return results.ToArray();
41 }
42
43 /// <summary>
44 /// Returns rows from requested tables formatted to facilitate testing.
45 /// If the table did not exist in the database, its list will be null.
46 /// </summary>
47 /// <param name="path"></param>
48 /// <param name="tables"></param>
49 /// <returns></returns>
50 public static Dictionary<string, List<string>> QueryDatabaseByTable(string path, string[] tables)
51 {
52 var results = new Dictionary<string, List<string>>();
53
54 if (tables?.Length > 0)
55 {
56 using (var db = new Database(path))
57 {
58 results = QueryDatabaseByTable(db, tables);
59 }
60 }
61
62 return results;
63 }
64
65 /// <summary>
66 /// Returns rows from requested tables formatted to facilitate testing.
67 /// If the table did not exist in the database, its list will be null.
68 /// </summary>
69 /// <param name="db"></param>
70 /// <param name="tables"></param>
71 /// <returns></returns>
72 public static Dictionary<string, List<string>> QueryDatabaseByTable(Database db, string[] tables)
73 {
74 var results = new Dictionary<string, List<string>>();
75
76 if (tables?.Length > 0)
77 {
78 var sb = new StringBuilder();
79
80 foreach (var table in tables)
81 {
82 if (table == "_SummaryInformation")
83 {
84 var entries = new List<string>();
85 results.Add(table, entries);
86
87 entries.Add($"Title\t{db.SummaryInfo.Title}");
88 entries.Add($"Subject\t{db.SummaryInfo.Subject}");
89 entries.Add($"Author\t{db.SummaryInfo.Author}");
90 entries.Add($"Keywords\t{db.SummaryInfo.Keywords}");
91 entries.Add($"Comments\t{db.SummaryInfo.Comments}");
92 entries.Add($"Template\t{db.SummaryInfo.Template}");
93 entries.Add($"CodePage\t{db.SummaryInfo.CodePage}");
94 entries.Add($"PageCount\t{db.SummaryInfo.PageCount}");
95 entries.Add($"WordCount\t{db.SummaryInfo.WordCount}");
96 entries.Add($"CharacterCount\t{db.SummaryInfo.CharacterCount}");
97 entries.Add($"Security\t{db.SummaryInfo.Security}");
98
99 continue;
100 }
101
102 if (!db.IsTablePersistent(table))
103 {
104 results.Add(table, null);
105 continue;
106 }
107
108 var rows = new List<string>();
109 results.Add(table, rows);
110
111 using (var view = db.OpenView("SELECT * FROM `{0}`", table))
112 {
113 view.Execute();
114
115 Record record;
116 while ((record = view.Fetch()) != null)
117 {
118 sb.Clear();
119
120 using (record)
121 {
122 for (var i = 0; i < record.FieldCount; ++i)
123 {
124 if (i > 0)
125 {
126 sb.Append("\t");
127 }
128
129 sb.Append(record[i + 1]?.ToString());
130 }
131 }
132
133 rows.Add(sb.ToString());
134 }
135 }
136
137 rows.Sort();
138 }
139 }
140
141 return results;
142 }
143
144 public static CabFileInfo[] GetCabinetFiles(string path)
145 {
146 var cab = new CabInfo(path);
147
148 var result = cab.GetFiles();
149
150 return result.Select(c => c).ToArray();
151 }
152
153 public static void ExtractStream(string path, string streamName, string outputPath)
154 {
155 Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
156
157 using (var db = new Database(path))
158 using (var view = db.OpenView("SELECT `Data` FROM `_Streams` WHERE `Name` = '{0}'", streamName))
159 {
160 view.Execute();
161
162 using (var record = view.Fetch())
163 {
164 record.GetStream(1, outputPath);
165 }
166 }
167 }
168
169 public static void ExtractSubStorage(string path, string subStorageName, string outputPath)
170 {
171 Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
172
173 using (var db = new Database(path))
174 using (var view = db.OpenView("SELECT `Name`, `Data` FROM `_Storages` WHERE `Name` = '{0}'", subStorageName))
175 {
176 view.Execute();
177
178 using (var record = view.Fetch())
179 {
180 var name = record.GetString(1);
181 record.GetStream(2, outputPath);
182 }
183 }
184 }
185
186 public static string[] GetSubStorageNames(string path)
187 {
188 var result = new List<string>();
189
190 using (var db = new Database(path))
191 using (var view = db.OpenView("SELECT `Name` FROM `_Storages`"))
192 {
193 view.Execute();
194
195 Record record;
196 while ((record = view.Fetch()) != null)
197 {
198 var name = record.GetString(1);
199 result.Add(name);
200 }
201 }
202
203 result.Sort();
204 return result.ToArray();
205 }
206 }
207}
diff --git a/src/internal/WixInternal.MSTestSupport/RobocopyRunner.cs b/src/internal/WixInternal.MSTestSupport/RobocopyRunner.cs
new file mode 100644
index 00000000..7ad8f6fe
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/RobocopyRunner.cs
@@ -0,0 +1,16 @@
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 WixInternal.MSTestSupport
4{
5 public class RobocopyRunner : ExternalExecutable
6 {
7 private static readonly RobocopyRunner Instance = new RobocopyRunner();
8
9 private RobocopyRunner() : base("robocopy") { }
10
11 public static ExternalExecutableResult Execute(string args)
12 {
13 return Instance.Run(args);
14 }
15 }
16}
diff --git a/src/internal/WixInternal.MSTestSupport/TestData.cs b/src/internal/WixInternal.MSTestSupport/TestData.cs
new file mode 100644
index 00000000..5f167a87
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/TestData.cs
@@ -0,0 +1,78 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6 using System.IO;
7 using System.Reflection;
8 using System.Runtime.CompilerServices;
9
10 public class TestData
11 {
12 public static void CreateFile(string path, long size, bool fill = false)
13 {
14 // Ensure the directory exists.
15 path = Path.GetFullPath(path);
16 Directory.CreateDirectory(Path.GetDirectoryName(path));
17
18 using (var file = File.OpenWrite(path))
19 {
20 if (fill)
21 {
22 var random = new Random();
23 var bytes = new byte[4096];
24 var generated = 0L;
25
26 // Put fill bytes in the file so it doesn't compress trivially.
27 while (generated < size)
28 {
29 var generate = (int)Math.Min(size - generated, bytes.Length);
30
31 random.NextBytes(bytes);
32
33 file.Write(bytes, 0, generate);
34
35 generated += generate;
36 }
37 }
38 else
39 {
40 file.SetLength(size);
41 }
42 }
43 }
44
45 public static string Get(params string[] paths)
46 {
47 var localPath = AppDomain.CurrentDomain.BaseDirectory;
48 return Path.Combine(localPath, Path.Combine(paths));
49 }
50
51 public static string GetUnitTestLogsFolder([CallerFilePath] string path = "", [CallerMemberName] string method = "")
52 {
53 var startingPath = AppDomain.CurrentDomain.BaseDirectory;
54 var buildPath = startingPath;
55
56 while (!String.IsNullOrEmpty(buildPath))
57 {
58 var folderName = Path.GetFileName(buildPath);
59 if (String.Equals("build", folderName, StringComparison.OrdinalIgnoreCase))
60 {
61 break;
62 }
63
64 buildPath = Path.GetDirectoryName(buildPath);
65 }
66
67 if (String.IsNullOrEmpty(buildPath))
68 {
69 throw new InvalidOperationException($"Could not find the 'build' folder in the test path: {startingPath}. Cannot get test logs folder without being able to find the build folder.");
70 }
71
72 var testLogsFolder = Path.Combine(buildPath, "logs", "UnitTests", $"{Path.GetFileNameWithoutExtension(path)}_{method}");
73 Directory.CreateDirectory(testLogsFolder);
74
75 return testLogsFolder;
76 }
77 }
78}
diff --git a/src/internal/WixInternal.MSTestSupport/TestDataFolderFileSystem.cs b/src/internal/WixInternal.MSTestSupport/TestDataFolderFileSystem.cs
new file mode 100644
index 00000000..1fea9665
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/TestDataFolderFileSystem.cs
@@ -0,0 +1,42 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6
7 /// <summary>
8 /// This class builds on top of DisposableFileSystem
9 /// to make it easy to write a test that needs a whole folder of test data copied to a temp location
10 /// that will automatically be cleaned up at the end of the test.
11 /// </summary>
12 public class TestDataFolderFileSystem : IDisposable
13 {
14 private DisposableFileSystem fileSystem;
15
16 public string BaseFolder { get; private set; }
17
18 public void Dispose()
19 {
20 this.fileSystem?.Dispose();
21 }
22
23 public void Initialize(string sourceDirectoryPath)
24 {
25 if (this.fileSystem != null)
26 {
27 throw new InvalidOperationException();
28 }
29 this.fileSystem = new DisposableFileSystem();
30
31 this.BaseFolder = this.fileSystem.GetFolder();
32
33 RobocopyFolder(sourceDirectoryPath, this.BaseFolder);
34 }
35
36 public static ExternalExecutableResult RobocopyFolder(string sourceFolderPath, string destinationFolderPath)
37 {
38 var args = $"\"{sourceFolderPath}\" \"{destinationFolderPath}\" /E /R:1 /W:1";
39 return RobocopyRunner.Execute(args);
40 }
41 }
42}
diff --git a/src/internal/WixInternal.MSTestSupport/VswhereRunner.cs b/src/internal/WixInternal.MSTestSupport/VswhereRunner.cs
new file mode 100644
index 00000000..f6ff2116
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/VswhereRunner.cs
@@ -0,0 +1,41 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6 using System.IO;
7
8 public class VswhereRunner : ExternalExecutable
9 {
10 private static readonly string VswhereRelativePath = @"Microsoft Visual Studio\Installer\vswhere.exe";
11
12 private static readonly object InitLock = new object();
13 private static bool Initialized;
14 private static VswhereRunner Instance;
15
16 public static ExternalExecutableResult Execute(string args, bool mergeErrorIntoOutput = false) =>
17 InitAndExecute(args, mergeErrorIntoOutput);
18
19 private static ExternalExecutableResult InitAndExecute(string args, bool mergeErrorIntoOutput)
20 {
21 lock (InitLock)
22 {
23 if (!Initialized)
24 {
25 Initialized = true;
26 var vswherePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), VswhereRelativePath);
27 if (!File.Exists(vswherePath))
28 {
29 throw new InvalidOperationException($"Failed to find vswhere at: {vswherePath}");
30 }
31
32 Instance = new VswhereRunner(vswherePath);
33 }
34 }
35
36 return Instance.Run(args, mergeErrorIntoOutput);
37 }
38
39 private VswhereRunner(string exePath) : base(exePath) { }
40 }
41}
diff --git a/src/internal/WixInternal.MSTestSupport/WixAssert.cs b/src/internal/WixInternal.MSTestSupport/WixAssert.cs
new file mode 100644
index 00000000..927ebee6
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/WixAssert.cs
@@ -0,0 +1,164 @@
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 WixInternal.MSTestSupport
4{
5 using Microsoft.VisualStudio.TestTools.UnitTesting;
6 using System;
7 using System.Collections.Generic;
8 using System.Linq;
9 using System.Text;
10 using System.Xml.Linq;
11
12 public class WixAssert
13 {
14 public static void CompareLineByLine(string[] expectedLines, string[] actualLines)
15 {
16 var lineNumber = 0;
17
18 for (; lineNumber < expectedLines.Length && lineNumber < actualLines.Length; ++lineNumber)
19 {
20 StringEqual($"{lineNumber}: {expectedLines[lineNumber]}", $"{lineNumber}: {actualLines[lineNumber]}");
21 }
22
23 var additionalExpectedLines = expectedLines.Length > lineNumber ? String.Join(Environment.NewLine, expectedLines.Skip(lineNumber).Select((s, i) => $"{lineNumber + i}: {s}")) : $"Missing {actualLines.Length - lineNumber} lines";
24 var additionalActualLines = actualLines.Length > lineNumber ? String.Join(Environment.NewLine, actualLines.Skip(lineNumber).Select((s, i) => $"{lineNumber + i}: {s}")) : $"Missing {expectedLines.Length - lineNumber} lines";
25
26 Assert.AreEqual<object>(additionalExpectedLines, additionalActualLines, StringObjectEqualityComparer.InvariantCulture);
27 }
28
29 public static void CompareXml(XContainer xExpected, XContainer xActual)
30 {
31 var expecteds = ComparableElements(xExpected);
32 var actuals = ComparableElements(xActual);
33
34 CompareLineByLine(expecteds.OrderBy(s => s).ToArray(), actuals.OrderBy(s => s).ToArray());
35 }
36
37 public static void CompareXml(string expectedPath, string actualPath)
38 {
39 var expectedDoc = XDocument.Load(expectedPath, LoadOptions.PreserveWhitespace | LoadOptions.SetBaseUri | LoadOptions.SetLineInfo);
40 var actualDoc = XDocument.Load(actualPath, LoadOptions.PreserveWhitespace | LoadOptions.SetBaseUri | LoadOptions.SetLineInfo);
41
42 CompareXml(expectedDoc, actualDoc);
43 }
44
45 private static IEnumerable<string> ComparableElements(XContainer container)
46 {
47 return container.Descendants().Select(x => $"{x.Name.LocalName}:{String.Join(",", x.Attributes().OrderBy(a => a.Name.LocalName).Select(a => $"{a.Name.LocalName}={ComparableAttribute(a)}"))}");
48 }
49
50 private static string ComparableAttribute(XAttribute attribute)
51 {
52 switch (attribute.Name.LocalName)
53 {
54 case "SourceFile":
55 return "<SourceFile>";
56 default:
57 return attribute.Value;
58 }
59 }
60
61 public static void StringCollectionEmpty(IList<string> collection)
62 {
63 if (collection.Count > 0)
64 {
65 Assert.Fail($"The collection was expected to be empty, but instead was [{Environment.NewLine}\"{String.Join($"\", {Environment.NewLine}\"", collection)}\"{Environment.NewLine}]");
66 }
67 }
68
69 public static void StringEqual(string expected, string actual, bool ignoreCase = false)
70 {
71 WixStringEqualException.ThrowIfNotEqual(expected, actual, ignoreCase);
72 }
73
74 public static void NotStringEqual(string expected, string actual, bool ignoreCase = false)
75 {
76 var comparer = ignoreCase ? StringObjectEqualityComparer.InvariantCultureIgnoreCase : StringObjectEqualityComparer.InvariantCulture;
77 Assert.AreNotEqual(expected, actual, comparer);
78 }
79
80 public static void Single<T>(IEnumerable<T> collection)
81 {
82 Assert.AreEqual(1, collection.Count());
83 }
84
85 public static void Single<T>(IEnumerable<T> collection, Func<T, bool> predicate)
86 {
87 var results = collection.Where(predicate);
88 Assert.AreEqual(1, results.Count());
89 }
90
91 public static void Empty<T>(IEnumerable<T> collection)
92 {
93 Assert.AreEqual(0, collection.Count());
94 }
95
96 public static void Empty<T>(IEnumerable<T> collection, Func<T, bool> predicate)
97 {
98 var results = collection.Where(predicate);
99 Assert.AreEqual(0, results.Count());
100 }
101 }
102
103 internal class StringObjectEqualityComparer : IEqualityComparer<object>
104 {
105 public static readonly StringObjectEqualityComparer InvariantCultureIgnoreCase = new StringObjectEqualityComparer(true);
106 public static readonly StringObjectEqualityComparer InvariantCulture = new StringObjectEqualityComparer(false);
107
108 private readonly StringComparer stringComparer;
109
110 public StringObjectEqualityComparer(bool ignoreCase)
111 {
112 this.stringComparer = ignoreCase ? StringComparer.InvariantCultureIgnoreCase : StringComparer.InvariantCulture;
113 }
114
115 public new bool Equals(object x, object y)
116 {
117 return this.stringComparer.Equals((string)x, (string)y);
118 }
119
120 public int GetHashCode(object obj)
121 {
122 return this.stringComparer.GetHashCode((string)obj);
123 }
124 }
125
126 public class WixStringEqualException : AssertFailedException
127 {
128 public WixStringEqualException(string userMessage) : base(userMessage) { }
129
130 public static void ThrowIfNotEqual(string expected, string actual, bool ignoreCase)
131 {
132 var comparer = ignoreCase ? StringObjectEqualityComparer.InvariantCultureIgnoreCase : StringObjectEqualityComparer.InvariantCulture;
133 if (comparer.Equals(expected, actual))
134 {
135 return;
136 }
137
138 var sbMessage = new StringBuilder();
139
140 try
141 {
142 Assert.AreEqual(expected, actual, ignoreCase);
143 }
144 catch (AssertFailedException xe)
145 {
146 // If either string is not completely in the message, then make sure it gets in there.
147 if (!xe.Message.Contains(expected) || !xe.Message.Contains(actual))
148 {
149 sbMessage.AppendLine(xe.Message);
150 sbMessage.AppendLine();
151 sbMessage.AppendFormat("Expected: {0}", expected);
152 sbMessage.AppendLine();
153 sbMessage.AppendFormat("Actual: {0}", actual);
154 }
155 else
156 {
157 throw;
158 }
159 }
160
161 throw new WixStringEqualException(sbMessage.ToString());
162 }
163 }
164}
diff --git a/src/internal/WixInternal.MSTestSupport/WixInternal.MSTestSupport.csproj b/src/internal/WixInternal.MSTestSupport/WixInternal.MSTestSupport.csproj
new file mode 100644
index 00000000..b48aacd4
--- /dev/null
+++ b/src/internal/WixInternal.MSTestSupport/WixInternal.MSTestSupport.csproj
@@ -0,0 +1,27 @@
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
6 <PropertyGroup>
7 <TargetFrameworks>net6.0;net472</TargetFrameworks>
8 <IsPackable>true</IsPackable>
9 <DebugType>embedded</DebugType>
10 <PublishRepositoryUrl>true</PublishRepositoryUrl>
11 <CreateDocumentationFile>true</CreateDocumentationFile>
12 <NoWarn>$(NoWarn);CS1591</NoWarn>
13 <SignOutput>false</SignOutput>
14 <IsWixTestSupportProject>true</IsWixTestSupportProject>
15 </PropertyGroup>
16
17 <ItemGroup>
18 <PackageReference Include="Microsoft.Build.Tasks.Core" />
19 <PackageReference Include="WixToolset.Dtf.WindowsInstaller" />
20 <PackageReference Include="WixToolset.Dtf.Compression" />
21 <PackageReference Include="WixToolset.Dtf.Compression.Cab" />
22 </ItemGroup>
23
24 <ItemGroup>
25 <PackageReference Include="MSTest.TestFramework" />
26 </ItemGroup>
27</Project>
diff --git a/src/internal/WixInternal.TestSupport/WixAssert.cs b/src/internal/WixInternal.TestSupport/WixAssert.cs
new file mode 100644
index 00000000..40355131
--- /dev/null
+++ b/src/internal/WixInternal.TestSupport/WixAssert.cs
@@ -0,0 +1,143 @@
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 WixInternal.MSTestSupport
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Text;
9 using System.Xml.Linq;
10 using Xunit;
11 using Xunit.Sdk;
12
13 public class WixAssert
14 {
15 public static void CompareLineByLine(string[] expectedLines, string[] actualLines)
16 {
17 var lineNumber = 0;
18
19 for (; lineNumber < expectedLines.Length && lineNumber < actualLines.Length; ++lineNumber)
20 {
21 StringEqual($"{lineNumber}: {expectedLines[lineNumber]}", $"{lineNumber}: {actualLines[lineNumber]}");
22 }
23
24 var additionalExpectedLines = expectedLines.Length > lineNumber ? String.Join(Environment.NewLine, expectedLines.Skip(lineNumber).Select((s, i) => $"{lineNumber + i}: {s}")) : $"Missing {actualLines.Length - lineNumber} lines";
25 var additionalActualLines = actualLines.Length > lineNumber ? String.Join(Environment.NewLine, actualLines.Skip(lineNumber).Select((s, i) => $"{lineNumber + i}: {s}")) : $"Missing {expectedLines.Length - lineNumber} lines";
26
27 Assert.Equal<object>(additionalExpectedLines, additionalActualLines, StringObjectEqualityComparer.InvariantCulture);
28 }
29
30 public static void CompareXml(XContainer xExpected, XContainer xActual)
31 {
32 var expecteds = ComparableElements(xExpected);
33 var actuals = ComparableElements(xActual);
34
35 CompareLineByLine(expecteds.OrderBy(s => s).ToArray(), actuals.OrderBy(s => s).ToArray());
36 }
37
38 public static void CompareXml(string expectedPath, string actualPath)
39 {
40 var expectedDoc = XDocument.Load(expectedPath, LoadOptions.PreserveWhitespace | LoadOptions.SetBaseUri | LoadOptions.SetLineInfo);
41 var actualDoc = XDocument.Load(actualPath, LoadOptions.PreserveWhitespace | LoadOptions.SetBaseUri | LoadOptions.SetLineInfo);
42
43 CompareXml(expectedDoc, actualDoc);
44 }
45
46 private static IEnumerable<string> ComparableElements(XContainer container)
47 {
48 return container.Descendants().Select(x => $"{x.Name.LocalName}:{String.Join(",", x.Attributes().OrderBy(a => a.Name.LocalName).Select(a => $"{a.Name.LocalName}={ComparableAttribute(a)}"))}");
49 }
50
51 private static string ComparableAttribute(XAttribute attribute)
52 {
53 switch (attribute.Name.LocalName)
54 {
55 case "SourceFile":
56 return "<SourceFile>";
57 default:
58 return attribute.Value;
59 }
60 }
61
62 public static void StringCollectionEmpty(IList<string> collection)
63 {
64 if (collection.Count > 0)
65 {
66 Assert.Fail($"The collection was expected to be empty, but instead was [{Environment.NewLine}\"{String.Join($"\", {Environment.NewLine}\"", collection)}\"{Environment.NewLine}]");
67 }
68 }
69
70 public static void StringEqual(string expected, string actual, bool ignoreCase = false)
71 {
72 WixStringEqualException.ThrowIfNotEqual(expected, actual, ignoreCase);
73 }
74
75 public static void NotStringEqual(string expected, string actual, bool ignoreCase = false)
76 {
77 var comparer = ignoreCase ? StringObjectEqualityComparer.InvariantCultureIgnoreCase : StringObjectEqualityComparer.InvariantCulture;
78 Assert.NotEqual(expected, actual, comparer);
79 }
80 }
81
82 internal class StringObjectEqualityComparer : IEqualityComparer<object>
83 {
84 public static readonly StringObjectEqualityComparer InvariantCultureIgnoreCase = new StringObjectEqualityComparer(true);
85 public static readonly StringObjectEqualityComparer InvariantCulture = new StringObjectEqualityComparer(false);
86
87 private readonly StringComparer stringComparer;
88
89 public StringObjectEqualityComparer(bool ignoreCase)
90 {
91 this.stringComparer = ignoreCase ? StringComparer.InvariantCultureIgnoreCase : StringComparer.InvariantCulture;
92 }
93
94 public new bool Equals(object x, object y)
95 {
96 return this.stringComparer.Equals((string)x, (string)y);
97 }
98
99 public int GetHashCode(object obj)
100 {
101 return this.stringComparer.GetHashCode((string)obj);
102 }
103 }
104
105 public class WixStringEqualException : XunitException
106 {
107 public WixStringEqualException(string userMessage) : base(userMessage) { }
108
109 public static void ThrowIfNotEqual(string expected, string actual, bool ignoreCase)
110 {
111 var comparer = ignoreCase ? StringObjectEqualityComparer.InvariantCultureIgnoreCase : StringObjectEqualityComparer.InvariantCulture;
112 if (comparer.Equals(expected, actual))
113 {
114 return;
115 }
116
117 var sbMessage = new StringBuilder();
118
119 try
120 {
121 Assert.Equal(expected, actual, ignoreCase);
122 }
123 catch (XunitException xe)
124 {
125 // If either string is not completely in the message, then make sure it gets in there.
126 if (!xe.Message.Contains(expected) || !xe.Message.Contains(actual))
127 {
128 sbMessage.AppendLine(xe.Message);
129 sbMessage.AppendLine();
130 sbMessage.AppendFormat("Expected: {0}", expected);
131 sbMessage.AppendLine();
132 sbMessage.AppendFormat("Actual: {0}", actual);
133 }
134 else
135 {
136 throw;
137 }
138 }
139
140 throw new WixStringEqualException(sbMessage.ToString());
141 }
142 }
143}
diff --git a/src/internal/WixInternal.TestSupport/XunitExtensions/WixAssert.cs b/src/internal/WixInternal.TestSupport/XunitExtensions/WixAssert.cs
index 5ac28de1..131d4e83 100644
--- a/src/internal/WixInternal.TestSupport/XunitExtensions/WixAssert.cs
+++ b/src/internal/WixInternal.TestSupport/XunitExtensions/WixAssert.cs
@@ -105,6 +105,32 @@ namespace WixInternal.TestSupport
105 Assert.NotEqual<object>(expected, actual, comparer); 105 Assert.NotEqual<object>(expected, actual, comparer);
106 } 106 }
107 107
108 public static void Single(IEnumerable<string> collection)
109 {
110 Assert.Single(collection);
111 // TODO: MSTEST: Assert.Equal(1, collection.Count());
112 }
113
114 public static void Single(IEnumerable<string> collection, Func<string, bool> predicate)
115 {
116 var results = collection.Where(predicate);
117 Assert.Single(results);
118 // TODO: MSTEST: Assert.Equal(1, results.Count());
119 }
120
121 public static void Empty(IEnumerable<string> collection)
122 {
123 Assert.Empty(collection);
124 // TODO: MSTEST: Assert.Equal(0, collection.Count());
125 }
126
127 public static void Empty(IEnumerable<string> collection, Func<string, bool> predicate)
128 {
129 var results = collection.Where(predicate);
130 Assert.Empty(results);
131 // TODO: MSTEST: Assert.Equal(0, results.Count());
132 }
133
108 // There appears to have been a bug in VC++, which might or might not have been partially 134 // There appears to have been a bug in VC++, which might or might not have been partially
109 // or completely corrected. It was unable to disambiguate a call to: 135 // or completely corrected. It was unable to disambiguate a call to:
110 // Xunit::Assert::Throws(System::Type^, System::Action^) 136 // Xunit::Assert::Throws(System::Type^, System::Action^)
diff --git a/src/internal/internal.sln b/src/internal/internal.sln
index e8d8db17..5514e543 100644
--- a/src/internal/internal.sln
+++ b/src/internal/internal.sln
@@ -9,6 +9,10 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WixInternal.TestSupport.Nat
9EndProject 9EndProject
10Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixInternal.BaseBuildTasks.Sources", "WixInternal.BaseBuildTasks.Sources\WixInternal.BaseBuildTasks.Sources.csproj", "{6B654490-AB0D-4F94-B564-DAA80044D5A3}" 10Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixInternal.BaseBuildTasks.Sources", "WixInternal.BaseBuildTasks.Sources\WixInternal.BaseBuildTasks.Sources.csproj", "{6B654490-AB0D-4F94-B564-DAA80044D5A3}"
11EndProject 11EndProject
12Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixInternal.XunitTestSupport", "WixInternal.XunitTestSupport\WixInternal.XunitTestSupport.csproj", "{AF7C4730-583B-46F8-9BB6-16D1F0330932}"
13EndProject
14Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixInternal.MSTestSupport", "WixInternal.MSTestSupport\WixInternal.MSTestSupport.csproj", "{E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}"
15EndProject
12Global 16Global
13 GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 Debug|Any CPU = Debug|Any CPU 18 Debug|Any CPU = Debug|Any CPU
@@ -65,6 +69,38 @@ Global
65 {6B654490-AB0D-4F94-B564-DAA80044D5A3}.Release|x64.Build.0 = Release|Any CPU 69 {6B654490-AB0D-4F94-B564-DAA80044D5A3}.Release|x64.Build.0 = Release|Any CPU
66 {6B654490-AB0D-4F94-B564-DAA80044D5A3}.Release|x86.ActiveCfg = Release|Any CPU 70 {6B654490-AB0D-4F94-B564-DAA80044D5A3}.Release|x86.ActiveCfg = Release|Any CPU
67 {6B654490-AB0D-4F94-B564-DAA80044D5A3}.Release|x86.Build.0 = Release|Any CPU 71 {6B654490-AB0D-4F94-B564-DAA80044D5A3}.Release|x86.Build.0 = Release|Any CPU
72 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
73 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Debug|Any CPU.Build.0 = Debug|Any CPU
74 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Debug|ARM64.ActiveCfg = Debug|Any CPU
75 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Debug|ARM64.Build.0 = Debug|Any CPU
76 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Debug|x64.ActiveCfg = Debug|Any CPU
77 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Debug|x64.Build.0 = Debug|Any CPU
78 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Debug|x86.ActiveCfg = Debug|Any CPU
79 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Debug|x86.Build.0 = Debug|Any CPU
80 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Release|Any CPU.ActiveCfg = Release|Any CPU
81 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Release|Any CPU.Build.0 = Release|Any CPU
82 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Release|ARM64.ActiveCfg = Release|Any CPU
83 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Release|ARM64.Build.0 = Release|Any CPU
84 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Release|x64.ActiveCfg = Release|Any CPU
85 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Release|x64.Build.0 = Release|Any CPU
86 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Release|x86.ActiveCfg = Release|Any CPU
87 {AF7C4730-583B-46F8-9BB6-16D1F0330932}.Release|x86.Build.0 = Release|Any CPU
88 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
89 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
90 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Debug|ARM64.ActiveCfg = Debug|Any CPU
91 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Debug|ARM64.Build.0 = Debug|Any CPU
92 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Debug|x64.ActiveCfg = Debug|Any CPU
93 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Debug|x64.Build.0 = Debug|Any CPU
94 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Debug|x86.ActiveCfg = Debug|Any CPU
95 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Debug|x86.Build.0 = Debug|Any CPU
96 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
97 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Release|Any CPU.Build.0 = Release|Any CPU
98 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Release|ARM64.ActiveCfg = Release|Any CPU
99 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Release|ARM64.Build.0 = Release|Any CPU
100 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Release|x64.ActiveCfg = Release|Any CPU
101 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Release|x64.Build.0 = Release|Any CPU
102 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Release|x86.ActiveCfg = Release|Any CPU
103 {E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}.Release|x86.Build.0 = Release|Any CPU
68 EndGlobalSection 104 EndGlobalSection
69 GlobalSection(SolutionProperties) = preSolution 105 GlobalSection(SolutionProperties) = preSolution
70 HideSolutionNode = FALSE 106 HideSolutionNode = FALSE
diff --git a/src/internal/internal_t.proj b/src/internal/internal_t.proj
index 7dd52354..ab96b6c8 100644
--- a/src/internal/internal_t.proj
+++ b/src/internal/internal_t.proj
@@ -2,6 +2,7 @@
2 <ItemGroup> 2 <ItemGroup>
3 <ProjectReference Include="WixInternal.BaseBuildTasks.Sources\WixInternal.BaseBuildTasks.Sources.csproj" Targets="Pack" /> 3 <ProjectReference Include="WixInternal.BaseBuildTasks.Sources\WixInternal.BaseBuildTasks.Sources.csproj" Targets="Pack" />
4 <ProjectReference Include="WixInternal.TestSupport\WixInternal.TestSupport.csproj" Targets="Pack" /> 4 <ProjectReference Include="WixInternal.TestSupport\WixInternal.TestSupport.csproj" Targets="Pack" />
5 <ProjectReference Include="WixInternal.MSTestSupport\WixInternal.MSTestSupport.csproj" Targets="Pack" />
5 <ProjectReference Include="WixInternal.TestSupport.Native\WixInternal.TestSupport.Native.vcxproj" Properties="Platform=x86" /> 6 <ProjectReference Include="WixInternal.TestSupport.Native\WixInternal.TestSupport.Native.vcxproj" Properties="Platform=x86" />
6 <ProjectReference Include="WixInternal.TestSupport.Native\WixInternal.TestSupport.Native.vcxproj" Properties="Platform=x64" /> 7 <ProjectReference Include="WixInternal.TestSupport.Native\WixInternal.TestSupport.Native.vcxproj" Properties="Platform=x64" />
7 </ItemGroup> 8 </ItemGroup>