From 6a24996a2e831cfe402398af65b31fb1ecd575a9 Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Thu, 22 Apr 2021 16:36:39 -0700 Subject: Move WixBuildTools into internal --- src/internal/WixBuildTools.TestSupport/Builder.cs | 70 +++++++++ .../DisposableFileSystem.cs | 93 +++++++++++ .../WixBuildTools.TestSupport/DotnetRunner.cs | 57 +++++++ .../ExternalExecutable.cs | 88 +++++++++++ .../ExternalExecutableResult.cs | 17 ++ .../WixBuildTools.TestSupport/FakeBuildEngine.cs | 33 ++++ .../WixBuildTools.TestSupport/MsbuildRunner.cs | 168 ++++++++++++++++++++ .../MsbuildRunnerResult.cs | 19 +++ src/internal/WixBuildTools.TestSupport/Pushd.cs | 46 ++++++ src/internal/WixBuildTools.TestSupport/Query.cs | 172 +++++++++++++++++++++ .../WixBuildTools.TestSupport/RobocopyRunner.cs | 16 ++ .../SucceededException.cs | 18 +++ src/internal/WixBuildTools.TestSupport/TestData.cs | 16 ++ .../TestDataFolderFileSystem.cs | 42 +++++ .../WixBuildTools.TestSupport/VswhereRunner.cs | 41 +++++ .../WixBuildTools.TestSupport/WixAssert.cs | 47 ++++++ .../WixBuildTools.TestSupport.csproj | 31 ++++ 17 files changed, 974 insertions(+) create mode 100644 src/internal/WixBuildTools.TestSupport/Builder.cs create mode 100644 src/internal/WixBuildTools.TestSupport/DisposableFileSystem.cs create mode 100644 src/internal/WixBuildTools.TestSupport/DotnetRunner.cs create mode 100644 src/internal/WixBuildTools.TestSupport/ExternalExecutable.cs create mode 100644 src/internal/WixBuildTools.TestSupport/ExternalExecutableResult.cs create mode 100644 src/internal/WixBuildTools.TestSupport/FakeBuildEngine.cs create mode 100644 src/internal/WixBuildTools.TestSupport/MsbuildRunner.cs create mode 100644 src/internal/WixBuildTools.TestSupport/MsbuildRunnerResult.cs create mode 100644 src/internal/WixBuildTools.TestSupport/Pushd.cs create mode 100644 src/internal/WixBuildTools.TestSupport/Query.cs create mode 100644 src/internal/WixBuildTools.TestSupport/RobocopyRunner.cs create mode 100644 src/internal/WixBuildTools.TestSupport/SucceededException.cs create mode 100644 src/internal/WixBuildTools.TestSupport/TestData.cs create mode 100644 src/internal/WixBuildTools.TestSupport/TestDataFolderFileSystem.cs create mode 100644 src/internal/WixBuildTools.TestSupport/VswhereRunner.cs create mode 100644 src/internal/WixBuildTools.TestSupport/WixAssert.cs create mode 100644 src/internal/WixBuildTools.TestSupport/WixBuildTools.TestSupport.csproj (limited to 'src/internal/WixBuildTools.TestSupport') diff --git a/src/internal/WixBuildTools.TestSupport/Builder.cs b/src/internal/WixBuildTools.TestSupport/Builder.cs new file mode 100644 index 00000000..ef0de8c9 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/Builder.cs @@ -0,0 +1,70 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + using System.Collections.Generic; + using System.IO; + + public class Builder + { + public Builder(string sourceFolder, Type extensionType = null, string[] bindPaths = null, string outputFile = null) + { + this.SourceFolder = sourceFolder; + this.ExtensionType = extensionType; + this.BindPaths = bindPaths; + this.OutputFile = outputFile ?? "test.msi"; + } + + public string[] BindPaths { get; set; } + + public Type ExtensionType { get; set; } + + public string OutputFile { get; set; } + + public string SourceFolder { get; } + + public string[] BuildAndQuery(Action buildFunc, params string[] tables) + { + var sourceFiles = Directory.GetFiles(this.SourceFolder, "*.wxs"); + var wxlFiles = Directory.GetFiles(this.SourceFolder, "*.wxl"); + + using (var fs = new DisposableFileSystem()) + { + var intermediateFolder = fs.GetFolder(); + var outputPath = Path.Combine(intermediateFolder, "bin", this.OutputFile); + + var args = new List + { + "build", + "-o", outputPath, + "-intermediateFolder", intermediateFolder, + }; + + if (this.ExtensionType != null) + { + args.Add("-ext"); + args.Add(Path.GetFullPath(new Uri(this.ExtensionType.Assembly.CodeBase).LocalPath)); + } + + args.AddRange(sourceFiles); + + foreach (var wxlFile in wxlFiles) + { + args.Add("-loc"); + args.Add(wxlFile); + } + + foreach (var bindPath in this.BindPaths) + { + args.Add("-bindpath"); + args.Add(bindPath); + } + + buildFunc(args.ToArray()); + + return Query.QueryDatabase(outputPath, tables); + } + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/DisposableFileSystem.cs b/src/internal/WixBuildTools.TestSupport/DisposableFileSystem.cs new file mode 100644 index 00000000..f096db72 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/DisposableFileSystem.cs @@ -0,0 +1,93 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + using System.Collections.Generic; + using System.IO; + + public class DisposableFileSystem : IDisposable + { + protected bool Disposed { get; private set; } + + private List CleanupPaths { get; } = new List(); + + public bool Keep { get; } + + public DisposableFileSystem(bool keep = false) + { + this.Keep = keep; + } + + protected string GetFile(bool create = false) + { + var path = Path.GetTempFileName(); + + if (!create) + { + File.Delete(path); + } + + this.CleanupPaths.Add(path); + + return path; + } + + public string GetFolder(bool create = false) + { + var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + if (create) + { + Directory.CreateDirectory(path); + } + + this.CleanupPaths.Add(path); + + return path; + } + + + #region // IDisposable + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (this.Disposed) + { + return; + } + + if (disposing && !this.Keep) + { + foreach (var path in this.CleanupPaths) + { + try + { + if (File.Exists(path)) + { + File.Delete(path); + } + else if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + catch + { + // Best effort delete, so ignore any failures. + } + } + } + + this.Disposed = true; + } + + #endregion + } +} diff --git a/src/internal/WixBuildTools.TestSupport/DotnetRunner.cs b/src/internal/WixBuildTools.TestSupport/DotnetRunner.cs new file mode 100644 index 00000000..82391178 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/DotnetRunner.cs @@ -0,0 +1,57 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + using System.Collections.Generic; + using System.IO; + + public class DotnetRunner : ExternalExecutable + { + private static readonly object InitLock = new object(); + private static bool Initialized; + private static DotnetRunner Instance; + + public static ExternalExecutableResult Execute(string command, string[] arguments = null) => + InitAndExecute(command, arguments); + + private static ExternalExecutableResult InitAndExecute(string command, string[] arguments) + { + lock (InitLock) + { + if (!Initialized) + { + Initialized = true; + var dotnetPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + if (String.IsNullOrEmpty(dotnetPath) || !File.Exists(dotnetPath)) + { + dotnetPath = "dotnet"; + } + + Instance = new DotnetRunner(dotnetPath); + } + } + + return Instance.ExecuteCore(command, arguments); + } + + private DotnetRunner(string exePath) : base(exePath) { } + + private ExternalExecutableResult ExecuteCore(string command, string[] arguments) + { + var total = new List + { + command, + }; + + if (arguments != null) + { + total.AddRange(arguments); + } + + var args = CombineArguments(total); + var mergeErrorIntoOutput = true; + return this.Run(args, mergeErrorIntoOutput); + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/ExternalExecutable.cs b/src/internal/WixBuildTools.TestSupport/ExternalExecutable.cs new file mode 100644 index 00000000..eb07aa13 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/ExternalExecutable.cs @@ -0,0 +1,88 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Text; + + public abstract class ExternalExecutable + { + private readonly string exePath; + + protected ExternalExecutable(string exePath) + { + this.exePath = exePath; + } + + protected ExternalExecutableResult Run(string args, bool mergeErrorIntoOutput = false, string workingDirectory = null) + { + var startInfo = new ProcessStartInfo(this.exePath, args) + { + CreateNoWindow = true, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + WorkingDirectory = workingDirectory ?? Path.GetDirectoryName(this.exePath), + }; + + using (var process = Process.Start(startInfo)) + { + // This implementation of merging the streams does not guarantee that lines are retrieved in the same order that they were written. + // If the process is simultaneously writing to both streams, this is impossible to do anyway. + var standardOutput = new ConcurrentQueue(); + var standardError = mergeErrorIntoOutput ? standardOutput : new ConcurrentQueue(); + + process.ErrorDataReceived += (s, e) => { if (e.Data != null) { standardError.Enqueue(e.Data); } }; + process.OutputDataReceived += (s, e) => { if (e.Data != null) { standardOutput.Enqueue(e.Data); } }; + + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + + process.WaitForExit(); + + return new ExternalExecutableResult + { + ExitCode = process.ExitCode, + StandardError = mergeErrorIntoOutput ? null : standardError.ToArray(), + StandardOutput = standardOutput.ToArray(), + StartInfo = startInfo, + }; + } + } + + // This is internal because it assumes backslashes aren't used as escape characters and there aren't any double quotes. + internal static string CombineArguments(IEnumerable arguments) + { + if (arguments == null) + { + return null; + } + + var sb = new StringBuilder(); + + foreach (var arg in arguments) + { + if (sb.Length > 0) + { + sb.Append(' '); + } + + if (arg.IndexOf(' ') > -1) + { + sb.Append("\""); + sb.Append(arg); + sb.Append("\""); + } + else + { + sb.Append(arg); + } + } + + return sb.ToString(); + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/ExternalExecutableResult.cs b/src/internal/WixBuildTools.TestSupport/ExternalExecutableResult.cs new file mode 100644 index 00000000..19b5183b --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/ExternalExecutableResult.cs @@ -0,0 +1,17 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System.Diagnostics; + + public class ExternalExecutableResult + { + public int ExitCode { get; set; } + + public string[] StandardError { get; set; } + + public string[] StandardOutput { get; set; } + + public ProcessStartInfo StartInfo { get; set; } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/FakeBuildEngine.cs b/src/internal/WixBuildTools.TestSupport/FakeBuildEngine.cs new file mode 100644 index 00000000..20545970 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/FakeBuildEngine.cs @@ -0,0 +1,33 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System.Collections; + using System.Text; + using Microsoft.Build.Framework; + + public class FakeBuildEngine : IBuildEngine + { + private readonly StringBuilder output = new StringBuilder(); + + public int ColumnNumberOfTaskNode => 0; + + public bool ContinueOnError => false; + + public int LineNumberOfTaskNode => 0; + + public string ProjectFileOfTaskNode => "fake_wix.targets"; + + public string Output => this.output.ToString(); + + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => throw new System.NotImplementedException(); + + public void LogCustomEvent(CustomBuildEventArgs e) => this.output.AppendLine(e.Message); + + public void LogErrorEvent(BuildErrorEventArgs e) => this.output.AppendLine(e.Message); + + public void LogMessageEvent(BuildMessageEventArgs e) => this.output.AppendLine(e.Message); + + public void LogWarningEvent(BuildWarningEventArgs e) => this.output.AppendLine(e.Message); + } +} diff --git a/src/internal/WixBuildTools.TestSupport/MsbuildRunner.cs b/src/internal/WixBuildTools.TestSupport/MsbuildRunner.cs new file mode 100644 index 00000000..35e53de6 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/MsbuildRunner.cs @@ -0,0 +1,168 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + using System.Collections.Generic; + using System.IO; + + public class MsbuildRunner : ExternalExecutable + { + private static readonly string VswhereFindArguments = "-property installationPath"; + private static readonly string Msbuild15RelativePath = @"MSBuild\15.0\Bin\MSBuild.exe"; + private static readonly string Msbuild15RelativePath64 = @"MSBuild\15.0\Bin\amd64\MSBuild.exe"; + private static readonly string MsbuildCurrentRelativePath = @"MSBuild\Current\Bin\MSBuild.exe"; + private static readonly string MsbuildCurrentRelativePath64 = @"MSBuild\Current\Bin\amd64\MSBuild.exe"; + + private static readonly object InitLock = new object(); + + private static bool Initialized; + private static MsbuildRunner Msbuild15Runner; + private static MsbuildRunner Msbuild15Runner64; + private static MsbuildRunner MsbuildCurrentRunner; + private static MsbuildRunner MsbuildCurrentRunner64; + + public static MsbuildRunnerResult Execute(string projectPath, string[] arguments = null, bool x64 = false) => + InitAndExecute(String.Empty, projectPath, arguments, x64); + + public static MsbuildRunnerResult ExecuteWithMsbuild15(string projectPath, string[] arguments = null, bool x64 = false) => + InitAndExecute("15", projectPath, arguments, x64); + + public static MsbuildRunnerResult ExecuteWithMsbuildCurrent(string projectPath, string[] arguments = null, bool x64 = false) => + InitAndExecute("Current", projectPath, arguments, x64); + + private static MsbuildRunnerResult InitAndExecute(string msbuildVersion, string projectPath, string[] arguments, bool x64) + { + lock (InitLock) + { + if (!Initialized) + { + Initialized = true; + var vswhereResult = VswhereRunner.Execute(VswhereFindArguments, true); + if (vswhereResult.ExitCode != 0) + { + throw new InvalidOperationException($"Failed to execute vswhere.exe, exit code: {vswhereResult.ExitCode}. Output:\r\n{String.Join("\r\n", vswhereResult.StandardOutput)}"); + } + + string msbuild15Path = null; + string msbuild15Path64 = null; + string msbuildCurrentPath = null; + string msbuildCurrentPath64 = null; + + foreach (var installPath in vswhereResult.StandardOutput) + { + if (msbuildCurrentPath == null) + { + var path = Path.Combine(installPath, MsbuildCurrentRelativePath); + if (File.Exists(path)) + { + msbuildCurrentPath = path; + } + } + + if (msbuildCurrentPath64 == null) + { + var path = Path.Combine(installPath, MsbuildCurrentRelativePath64); + if (File.Exists(path)) + { + msbuildCurrentPath64 = path; + } + } + + if (msbuild15Path == null) + { + var path = Path.Combine(installPath, Msbuild15RelativePath); + if (File.Exists(path)) + { + msbuild15Path = path; + } + } + + if (msbuild15Path64 == null) + { + var path = Path.Combine(installPath, Msbuild15RelativePath64); + if (File.Exists(path)) + { + msbuild15Path64 = path; + } + } + } + + if (msbuildCurrentPath != null) + { + MsbuildCurrentRunner = new MsbuildRunner(msbuildCurrentPath); + } + + if (msbuildCurrentPath64 != null) + { + MsbuildCurrentRunner64 = new MsbuildRunner(msbuildCurrentPath64); + } + + if (msbuild15Path != null) + { + Msbuild15Runner = new MsbuildRunner(msbuild15Path); + } + + if (msbuild15Path64 != null) + { + Msbuild15Runner64 = new MsbuildRunner(msbuild15Path64); + } + } + } + + MsbuildRunner runner; + switch (msbuildVersion) + { + case "15": + { + runner = x64 ? Msbuild15Runner64 : Msbuild15Runner; + break; + } + case "Current": + { + runner = x64 ? MsbuildCurrentRunner64 : MsbuildCurrentRunner; + break; + } + default: + { + runner = x64 ? MsbuildCurrentRunner64 ?? Msbuild15Runner64 + : MsbuildCurrentRunner ?? Msbuild15Runner; + break; + } + } + + if (runner == null) + { + throw new InvalidOperationException($"Failed to find an installed{(x64 ? " 64-bit" : String.Empty)} MSBuild{msbuildVersion}"); + } + + return runner.ExecuteCore(projectPath, arguments); + } + + private MsbuildRunner(string exePath) : base(exePath) { } + + private MsbuildRunnerResult ExecuteCore(string projectPath, string[] arguments) + { + var total = new List + { + projectPath, + }; + + if (arguments != null) + { + total.AddRange(arguments); + } + + var args = CombineArguments(total); + var mergeErrorIntoOutput = true; + var workingFolder = Path.GetDirectoryName(projectPath); + var result = this.Run(args, mergeErrorIntoOutput, workingFolder); + + return new MsbuildRunnerResult + { + ExitCode = result.ExitCode, + Output = result.StandardOutput, + }; + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/MsbuildRunnerResult.cs b/src/internal/WixBuildTools.TestSupport/MsbuildRunnerResult.cs new file mode 100644 index 00000000..5610987e --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/MsbuildRunnerResult.cs @@ -0,0 +1,19 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + using Xunit; + + public class MsbuildRunnerResult + { + public int ExitCode { get; set; } + + public string[] Output { get; set; } + + public void AssertSuccess() + { + Assert.True(0 == this.ExitCode, $"MSBuild failed unexpectedly. Output:\r\n{String.Join("\r\n", this.Output)}"); + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/Pushd.cs b/src/internal/WixBuildTools.TestSupport/Pushd.cs new file mode 100644 index 00000000..d0545215 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/Pushd.cs @@ -0,0 +1,46 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + using System.IO; + + public class Pushd : IDisposable + { + protected bool Disposed { get; private set; } + + public Pushd(string path) + { + this.PreviousDirectory = Directory.GetCurrentDirectory(); + + Directory.SetCurrentDirectory(path); + } + + public string PreviousDirectory { get; } + + #region // IDisposable + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (this.Disposed) + { + return; + } + + if (disposing) + { + Directory.SetCurrentDirectory(this.PreviousDirectory); + } + + this.Disposed = true; + } + + #endregion + } +} diff --git a/src/internal/WixBuildTools.TestSupport/Query.cs b/src/internal/WixBuildTools.TestSupport/Query.cs new file mode 100644 index 00000000..101a8890 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/Query.cs @@ -0,0 +1,172 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using WixToolset.Dtf.Compression.Cab; + using WixToolset.Dtf.WindowsInstaller; + + public class Query + { + public static string[] QueryDatabase(string path, string[] tables) + { + var results = new List(); + var resultsByTable = QueryDatabaseByTable(path, tables); + var sortedTables = tables.ToList(); + sortedTables.Sort(); + foreach (var tableName in sortedTables) + { + var rows = resultsByTable[tableName]; + rows?.ForEach(r => results.Add($"{tableName}:{r}")); + } + return results.ToArray(); + } + + /// + /// Returns rows from requested tables formatted to facilitate testing. + /// If the table did not exist in the database, its list will be null. + /// + /// + /// + /// + public static Dictionary> QueryDatabaseByTable(string path, string[] tables) + { + var results = new Dictionary>(); + + if (tables?.Length > 0) + { + var sb = new StringBuilder(); + using (var db = new Database(path)) + { + foreach (var table in tables) + { + if (table == "_SummaryInformation") + { + var entries = new List(); + results.Add(table, entries); + + entries.Add($"Title\t{db.SummaryInfo.Title}"); + entries.Add($"Subject\t{db.SummaryInfo.Subject}"); + entries.Add($"Author\t{db.SummaryInfo.Author}"); + entries.Add($"Keywords\t{db.SummaryInfo.Keywords}"); + entries.Add($"Comments\t{db.SummaryInfo.Comments}"); + entries.Add($"Template\t{db.SummaryInfo.Template}"); + entries.Add($"CodePage\t{db.SummaryInfo.CodePage}"); + entries.Add($"PageCount\t{db.SummaryInfo.PageCount}"); + entries.Add($"WordCount\t{db.SummaryInfo.WordCount}"); + entries.Add($"CharacterCount\t{db.SummaryInfo.CharacterCount}"); + entries.Add($"Security\t{db.SummaryInfo.Security}"); + + continue; + } + + if (!db.IsTablePersistent(table)) + { + results.Add(table, null); + continue; + } + + var rows = new List(); + results.Add(table, rows); + + using (var view = db.OpenView("SELECT * FROM `{0}`", table)) + { + view.Execute(); + + Record record; + while ((record = view.Fetch()) != null) + { + sb.Clear(); + + using (record) + { + for (var i = 0; i < record.FieldCount; ++i) + { + if (i > 0) + { + sb.Append("\t"); + } + + sb.Append(record[i + 1]?.ToString()); + } + } + + rows.Add(sb.ToString()); + } + } + rows.Sort(); + } + } + } + + return results; + } + + public static CabFileInfo[] GetCabinetFiles(string path) + { + var cab = new CabInfo(path); + + var result = cab.GetFiles(); + + return result.Select(c => c).ToArray(); + } + + public static void ExtractStream(string path, string streamName, string outputPath) + { + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + + using (var db = new Database(path)) + using (var view = db.OpenView("SELECT `Data` FROM `_Streams` WHERE `Name` = '{0}'", streamName)) + { + view.Execute(); + + using (var record = view.Fetch()) + { + record.GetStream(1, outputPath); + } + } + } + + public static void ExtractSubStorage(string path, string subStorageName, string outputPath) + { + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + + using (var db = new Database(path)) + using (var view = db.OpenView("SELECT `Name`, `Data` FROM `_Storages` WHERE `Name` = '{0}'", subStorageName)) + { + view.Execute(); + + using (var record = view.Fetch()) + { + var name = record.GetString(1); + record.GetStream(2, outputPath); + } + } + } + + public static string[] GetSubStorageNames(string path) + { + var result = new List(); + + using (var db = new Database(path)) + using (var view = db.OpenView("SELECT `Name` FROM `_Storages`")) + { + view.Execute(); + + Record record; + while ((record = view.Fetch()) != null) + { + var name = record.GetString(1); + result.Add(name); + } + } + + result.Sort(); + return result.ToArray(); + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/RobocopyRunner.cs b/src/internal/WixBuildTools.TestSupport/RobocopyRunner.cs new file mode 100644 index 00000000..49d53351 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/RobocopyRunner.cs @@ -0,0 +1,16 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + public class RobocopyRunner : ExternalExecutable + { + private static readonly RobocopyRunner Instance = new RobocopyRunner(); + + private RobocopyRunner() : base("robocopy") { } + + public static ExternalExecutableResult Execute(string args) + { + return Instance.Run(args); + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/SucceededException.cs b/src/internal/WixBuildTools.TestSupport/SucceededException.cs new file mode 100644 index 00000000..00b31d68 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/SucceededException.cs @@ -0,0 +1,18 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + using Xunit.Sdk; + + public class SucceededException : XunitException + { + public SucceededException(int hr, string userMessage) + : base(String.Format("WixAssert.Succeeded() Failure\r\n" + + "HRESULT: 0x{0:X8}\r\n" + + "Message: {1}", + hr, userMessage)) + { + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/TestData.cs b/src/internal/WixBuildTools.TestSupport/TestData.cs new file mode 100644 index 00000000..8587330d --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/TestData.cs @@ -0,0 +1,16 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + using System.IO; + + public class TestData + { + public static string Get(params string[] paths) + { + var localPath = Path.GetDirectoryName(new Uri(System.Reflection.Assembly.GetCallingAssembly().CodeBase).LocalPath); + return Path.Combine(localPath, Path.Combine(paths)); + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/TestDataFolderFileSystem.cs b/src/internal/WixBuildTools.TestSupport/TestDataFolderFileSystem.cs new file mode 100644 index 00000000..8d670bf0 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/TestDataFolderFileSystem.cs @@ -0,0 +1,42 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + + /// + /// This class builds on top of DisposableFileSystem + /// to make it easy to write a test that needs a whole folder of test data copied to a temp location + /// that will automatically be cleaned up at the end of the test. + /// + public class TestDataFolderFileSystem : IDisposable + { + private DisposableFileSystem fileSystem; + + public string BaseFolder { get; private set; } + + public void Dispose() + { + this.fileSystem?.Dispose(); + } + + public void Initialize(string sourceDirectoryPath) + { + if (this.fileSystem != null) + { + throw new InvalidOperationException(); + } + this.fileSystem = new DisposableFileSystem(); + + this.BaseFolder = this.fileSystem.GetFolder(); + + RobocopyFolder(sourceDirectoryPath, this.BaseFolder); + } + + private static ExternalExecutableResult RobocopyFolder(string sourceFolderPath, string destinationFolderPath) + { + var args = $"\"{sourceFolderPath}\" \"{destinationFolderPath}\" /E /R:1 /W:1"; + return RobocopyRunner.Execute(args); + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/VswhereRunner.cs b/src/internal/WixBuildTools.TestSupport/VswhereRunner.cs new file mode 100644 index 00000000..0197e125 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/VswhereRunner.cs @@ -0,0 +1,41 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + using System.IO; + + public class VswhereRunner : ExternalExecutable + { + private static readonly string VswhereRelativePath = @"Microsoft Visual Studio\Installer\vswhere.exe"; + + private static readonly object InitLock = new object(); + private static bool Initialized; + private static VswhereRunner Instance; + + public static ExternalExecutableResult Execute(string args, bool mergeErrorIntoOutput = false) => + InitAndExecute(args, mergeErrorIntoOutput); + + private static ExternalExecutableResult InitAndExecute(string args, bool mergeErrorIntoOutput) + { + lock (InitLock) + { + if (!Initialized) + { + Initialized = true; + var vswherePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), VswhereRelativePath); + if (!File.Exists(vswherePath)) + { + throw new InvalidOperationException($"Failed to find vswhere at: {vswherePath}"); + } + + Instance = new VswhereRunner(vswherePath); + } + } + + return Instance.Run(args, mergeErrorIntoOutput); + } + + private VswhereRunner(string exePath) : base(exePath) { } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/WixAssert.cs b/src/internal/WixBuildTools.TestSupport/WixAssert.cs new file mode 100644 index 00000000..5638a787 --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/WixAssert.cs @@ -0,0 +1,47 @@ +// 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. + +namespace WixBuildTools.TestSupport +{ + using System; + using System.Linq; + using System.Xml.Linq; + using Xunit; + + public class WixAssert : Assert + { + public static void CompareLineByLine(string[] expectedLines, string[] actualLines) + { + for (var i = 0; i < expectedLines.Length; ++i) + { + Assert.True(actualLines.Length > i, $"{i}: expectedLines longer than actualLines"); + Assert.Equal($"{i}: {expectedLines[i]}", $"{i}: {actualLines[i]}"); + } + + Assert.True(expectedLines.Length == actualLines.Length, "actualLines longer than expectedLines"); + } + + public static void CompareXml(XContainer xExpected, XContainer xActual) + { + var expecteds = xExpected.Descendants().Select(x => $"{x.Name.LocalName}:{String.Join(",", x.Attributes().OrderBy(a => a.Name.LocalName).Select(a => $"{a.Name.LocalName}={a.Value}"))}"); + var actuals = xActual.Descendants().Select(x => $"{x.Name.LocalName}:{String.Join(",", x.Attributes().OrderBy(a => a.Name.LocalName).Select(a => $"{a.Name.LocalName}={a.Value}"))}"); + + CompareLineByLine(expecteds.OrderBy(s => s).ToArray(), actuals.OrderBy(s => s).ToArray()); + } + + public static void CompareXml(string expectedPath, string actualPath) + { + var expectedDoc = XDocument.Load(expectedPath, LoadOptions.PreserveWhitespace | LoadOptions.SetBaseUri | LoadOptions.SetLineInfo); + var actualDoc = XDocument.Load(actualPath, LoadOptions.PreserveWhitespace | LoadOptions.SetBaseUri | LoadOptions.SetLineInfo); + + CompareXml(expectedDoc, actualDoc); + } + + public static void Succeeded(int hr, string format, params object[] formatArgs) + { + if (0 > hr) + { + throw new SucceededException(hr, String.Format(format, formatArgs)); + } + } + } +} diff --git a/src/internal/WixBuildTools.TestSupport/WixBuildTools.TestSupport.csproj b/src/internal/WixBuildTools.TestSupport/WixBuildTools.TestSupport.csproj new file mode 100644 index 00000000..f59e5eca --- /dev/null +++ b/src/internal/WixBuildTools.TestSupport/WixBuildTools.TestSupport.csproj @@ -0,0 +1,31 @@ + + + + + + + netstandard2.0;net461;net472 + true + embedded + true + true + CS1591 + + + + + + + + + + + + + + + + + + + -- cgit v1.2.3-55-g6feb