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> |
