diff options
author | Bob Arnson <bob@firegiant.com> | 2025-02-14 22:29:39 -0500 |
---|---|---|
committer | Bob Arnson <github@bobs.org> | 2025-03-03 14:25:07 -0500 |
commit | ca6e44d496b0c589fdaabad69a00643f539c47cd (patch) | |
tree | edf84727cecfc03092a2851b465d97622c5048eb /src/internal | |
parent | ba7fd5837ea149b2e319cc577fad27ce1162a064 (diff) | |
download | wix-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')
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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
3 | namespace 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 | |||
9 | EndProject | 9 | EndProject |
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixInternal.BaseBuildTasks.Sources", "WixInternal.BaseBuildTasks.Sources\WixInternal.BaseBuildTasks.Sources.csproj", "{6B654490-AB0D-4F94-B564-DAA80044D5A3}" | 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixInternal.BaseBuildTasks.Sources", "WixInternal.BaseBuildTasks.Sources\WixInternal.BaseBuildTasks.Sources.csproj", "{6B654490-AB0D-4F94-B564-DAA80044D5A3}" |
11 | EndProject | 11 | EndProject |
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixInternal.XunitTestSupport", "WixInternal.XunitTestSupport\WixInternal.XunitTestSupport.csproj", "{AF7C4730-583B-46F8-9BB6-16D1F0330932}" | ||
13 | EndProject | ||
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixInternal.MSTestSupport", "WixInternal.MSTestSupport\WixInternal.MSTestSupport.csproj", "{E70898F2-8D08-4FCE-9CFF-EF1792FCA2E2}" | ||
15 | EndProject | ||
12 | Global | 16 | Global |
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> |