From a8c65177b0b1d5551ecc7f5714e6d805852a5436 Mon Sep 17 00:00:00 2001 From: Christopher Wolf Date: Sun, 7 Jun 2020 15:01:22 +0200 Subject: [PATCH] Support dedicated installation package directory Introduced PackagesCrawler to encapsulate logic that will read all installation packages full file paths. It enables the DeployClient to read installation packages from a defined directory. It's no more necessary to save installation packages beside the DeployClient.exe. Encapsulated code in own method, that will write packages information to console and cancels the execution if no packages were found in packages directory, to get better readable code. Added unit tests for new PackagesCrawler component. Migrated Solution to Visual Studio 2019. Updated DeployClient project to .Net Framework 4.8 to get it up to date. Related issue: #26 --- DeployClient.Tests/DeployClient.Tests.csproj | 87 +++++++ DeployClient.Tests/PackageCrawlerTests.cs | 245 ++++++++++++++++++ DeployClient.Tests/Properties/AssemblyInfo.cs | 20 ++ DeployClient.Tests/packages.config | 8 + DeployClient/App.config | 2 +- DeployClient/DeployClient.csproj | 6 +- DeployClient/PackageCrawler.cs | 63 +++++ DeployClient/Program.cs | 67 +++-- DeployClient/Properties/AssemblyInfo.cs | 2 + DeployClient/packages.config | 1 + PolyDeploy.sln | 15 +- 11 files changed, 484 insertions(+), 32 deletions(-) create mode 100644 DeployClient.Tests/DeployClient.Tests.csproj create mode 100644 DeployClient.Tests/PackageCrawlerTests.cs create mode 100644 DeployClient.Tests/Properties/AssemblyInfo.cs create mode 100644 DeployClient.Tests/packages.config create mode 100644 DeployClient/PackageCrawler.cs diff --git a/DeployClient.Tests/DeployClient.Tests.csproj b/DeployClient.Tests/DeployClient.Tests.csproj new file mode 100644 index 0000000..902ec85 --- /dev/null +++ b/DeployClient.Tests/DeployClient.Tests.csproj @@ -0,0 +1,87 @@ + + + + + + Debug + AnyCPU + {2F81EBF7-A395-4716-8B1C-23FE9EE8B7CD} + Library + Properties + DeployClient.Tests + DeployClient.Tests + v4.8 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\FluentAssertions.5.10.3\lib\net47\FluentAssertions.dll + + + ..\packages\MSTest.TestFramework.2.1.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.2.1.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + ..\packages\System.IO.Abstractions.11.0.7\lib\net461\System.IO.Abstractions.dll + + + ..\packages\System.IO.Abstractions.TestingHelpers.11.0.7\lib\net461\System.IO.Abstractions.TestingHelpers.dll + + + + + + + + + + + + + + {b6122ccd-75f9-4def-8daa-e11789d6d6d8} + DeployClient + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/DeployClient.Tests/PackageCrawlerTests.cs b/DeployClient.Tests/PackageCrawlerTests.cs new file mode 100644 index 0000000..c9d8345 --- /dev/null +++ b/DeployClient.Tests/PackageCrawlerTests.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DeployClient.Tests +{ + [TestClass] + public class PackageCrawlerTests + { + private string _currentExecutionPath; + + + [TestInitialize] + public void Initialize() + { + _currentExecutionPath = $@"{Environment.CurrentDirectory}\DeployClient"; + } + + + #region constructor tests + + [TestMethod] + public void FileCrawler_Should_Use_Execution_Directory_When_Null_Argument() + { + // Arrange + var mockFileSystem = GetBasicPreparedMockFileSystem(_currentExecutionPath); + + // Act + var fileCrawler = new PackageCrawler(mockFileSystem, null); + + // Assert + fileCrawler.PackageDirectoryPath.Should().Be(_currentExecutionPath); + } + + [TestMethod] + public void FileCrawler_Should_Use_Execution_Directory_When_Empty_String_Argument() + { + // Arrange + var mockFileSystem = GetBasicPreparedMockFileSystem(_currentExecutionPath); + + // Act + var fileCrawler = new PackageCrawler(mockFileSystem, string.Empty); + + // Assert + fileCrawler.PackageDirectoryPath.Should().Be(_currentExecutionPath); + } + + [TestMethod] + public void FileCrawler_Should_Use_Execution_Directory_When_Whitespaces_Argument() + { + // Arrange + var mockFileSystem = GetBasicPreparedMockFileSystem(_currentExecutionPath); + + // Act + var fileCrawler = new PackageCrawler(mockFileSystem, " "); + + // Assert + fileCrawler.PackageDirectoryPath.Should().Be(_currentExecutionPath); + } + + [TestMethod] + public void FileCrawler_Should_Throw_Exception_When_Directory_Not_Exist() + { + // Arrange + var mockFileSystem = GetBasicPreparedMockFileSystem(_currentExecutionPath); + + // Act + // ReSharper disable once ObjectCreationAsStatement + Action initialization = () => new PackageCrawler(mockFileSystem, "FooBar"); + + // Assert + initialization.Should().Throw(); + } + + [TestMethod] + public void FileCrawler_Should_Initialize_With_Provided_Directory_When_Directory_Exist() + { + // Arrange + const string directoryName = "Packages"; + + var mockFileSystem = GetBasicPreparedMockFileSystem(_currentExecutionPath); + mockFileSystem.Directory.CreateDirectory(directoryName); + + // Act + var fileCrawler = new PackageCrawler(mockFileSystem, directoryName); + + // Assert + fileCrawler.PackageDirectoryPath.Should().Be($@"{_currentExecutionPath}\{directoryName}"); + } + + + + [TestMethod] + public void FileCrawler_Should_Initialize_With_Provided_Directory_When_Directory_Exist_And_Full_Path() + { + // Arrange + const string directoryName = "Packages"; + + var mockFileSystem = GetBasicPreparedMockFileSystem(_currentExecutionPath); + var directory = mockFileSystem.Directory.CreateDirectory(directoryName); + + var packageDirectoryPath = directory.FullName; + + // Act + var fileCrawler = new PackageCrawler(mockFileSystem, packageDirectoryPath); + + // Assert + fileCrawler.PackageDirectoryPath.Should().Be(packageDirectoryPath); + } + + #endregion + + #region GetPackagesFullPaths tests + + [TestMethod] + public void GetPackagesFullPaths_Should_Return_Empty_Enumerable_When_Directory_Empty() + { + // Arrange + const string directoryName = "Packages"; + + var mockFileSystem = GetBasicPreparedMockFileSystem(_currentExecutionPath); + mockFileSystem.Directory.CreateDirectory(directoryName); + + var packageCrawler = new PackageCrawler(mockFileSystem, directoryName); + + // Act + var packages = packageCrawler.GetPackagesFullPaths(); + + // Assert + packages.Should().BeEmpty(); + } + + [TestMethod] + public void GetPackagesFullPaths_Should_Return_Zip_Files_Full_Paths_When_Directory_Contains_Zip_Files_Only() + { + // Arrange + const string directoryName = "Packages"; + + var mockFiles = GetBasicMockFiles(); + mockFiles.Add($@"{_currentExecutionPath}\Packages\TestPackage_1.zip", new MockFileData("#1 fake zip file.")); + mockFiles.Add($@"{_currentExecutionPath}\Packages\TestPackage_2.zip", new MockFileData("#2 fake zip file.")); + mockFiles.Add($@"{_currentExecutionPath}\Packages\TestPackage_3.zip", new MockFileData("#3 fake zip file.")); + + var mockFileSystem = new MockFileSystem(mockFiles, _currentExecutionPath); + + var packageCrawler = new PackageCrawler(mockFileSystem, directoryName); + + // Act + var packages = packageCrawler.GetPackagesFullPaths(); + + // Assert + packages.Should().HaveCount(3); + } + + [TestMethod] + public void GetPackagesFullPaths_Should_Return_Only_Zip_Files_Full_Paths_When_Directory_Contains_Different_File_Kinds() + { + // Arrange + const string directoryName = "Packages"; + + var mockFiles = GetBasicMockFiles(); + mockFiles.Add($@"{_currentExecutionPath}\Packages\TestPackage.zip", new MockFileData("A fake zip file.")); + mockFiles.Add($@"{_currentExecutionPath}\Packages\TestTextFile.txt", new MockFileData("Just a text file.")); + + var mockFileSystem = new MockFileSystem(mockFiles, _currentExecutionPath); + + var packageCrawler = new PackageCrawler(mockFileSystem, directoryName); + + // Act + var packages = packageCrawler.GetPackagesFullPaths(); + + // Assert + packages.Should().HaveCount(1); + } + + [TestMethod] + public void GetPackagesFullPaths_Should_Return_Zip_Files_Full_Paths_When_Directory_Contains_Different_File_Kinds_And_Full_Path_Initialization() + { + // Arrange + var packageDirectoryPath = $@"{_currentExecutionPath}\Packages"; + + var mockFiles = GetBasicMockFiles(); + mockFiles.Add($@"{packageDirectoryPath}\TestPackage.zip", new MockFileData("A fake zip file.")); + mockFiles.Add($@"{packageDirectoryPath}\TestTextFile.txt", new MockFileData("Just a text file.")); + + var mockFileSystem = new MockFileSystem(mockFiles, _currentExecutionPath); + + var packageCrawler = new PackageCrawler(mockFileSystem, packageDirectoryPath); + + // Act + var packages = packageCrawler.GetPackagesFullPaths(); + + // Assert + packages.Should().HaveCount(1); + } + + [TestMethod] + public void GetPackagesFullPaths_Should_Return_Zip_Files_Of_Top_Directory_Only_When_Sub_Directory_Exists_With_Zip_Files() + { + // Arrange + const string directoryName = "Packages"; + + var mockFiles = GetBasicMockFiles(); + mockFiles.Add($@"{_currentExecutionPath}\Packages\TestPackage.zip", new MockFileData("A fake zip file.")); + mockFiles.Add($@"{_currentExecutionPath}\Packages\SubDirectory\SubDir_TestPackage.zip", new MockFileData("A fake zip file from sub-directory.")); + + var mockFileSystem = new MockFileSystem(mockFiles, _currentExecutionPath); + + var packageCrawler = new PackageCrawler(mockFileSystem, directoryName); + + // Act + var packages = packageCrawler.GetPackagesFullPaths(); + + // Assert + packages.Should().HaveCount(1); + } + + #endregion + + + #region helper methods + + private IFileSystem GetBasicPreparedMockFileSystem(string currentDirectoryPath) + { + return new MockFileSystem(GetBasicMockFiles(), currentDirectoryPath); + } + + private IDictionary GetBasicMockFiles() + { + return new Dictionary + { + { + $@"{_currentExecutionPath}\DeployClient_dummy.txt", + new MockFileData("Represents the executable to know where we are.") + } + }; + } + + #endregion + } +} diff --git a/DeployClient.Tests/Properties/AssemblyInfo.cs b/DeployClient.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..42d1e25 --- /dev/null +++ b/DeployClient.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("DeployClient.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DeployClient.Tests")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("2f81ebf7-a395-4716-8b1c-23fe9ee8b7cd")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/DeployClient.Tests/packages.config b/DeployClient.Tests/packages.config new file mode 100644 index 0000000..8b45c61 --- /dev/null +++ b/DeployClient.Tests/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/DeployClient/App.config b/DeployClient/App.config index 796693c..9091ff8 100644 --- a/DeployClient/App.config +++ b/DeployClient/App.config @@ -6,7 +6,7 @@ - + diff --git a/DeployClient/DeployClient.csproj b/DeployClient/DeployClient.csproj index db85a3a..3f2b03e 100644 --- a/DeployClient/DeployClient.csproj +++ b/DeployClient/DeployClient.csproj @@ -8,7 +8,7 @@ Exe DeployClient DeployClient - v4.5 + v4.8 512 true @@ -41,6 +41,9 @@ + + ..\packages\System.IO.Abstractions.11.0.7\lib\net461\System.IO.Abstractions.dll + ..\packages\System.ValueTuple.4.3.0\lib\netstandard1.0\System.ValueTuple.dll @@ -56,6 +59,7 @@ + diff --git a/DeployClient/PackageCrawler.cs b/DeployClient/PackageCrawler.cs new file mode 100644 index 0000000..8c2e573 --- /dev/null +++ b/DeployClient/PackageCrawler.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; + +namespace DeployClient +{ + internal class PackageCrawler + { + private readonly IFileSystem _fileSystem; + private readonly IDirectoryInfo _packageDirectoryInfo; + + + + /// + /// The directory that contains to installing files. + /// + public string PackageDirectoryPath => _packageDirectoryInfo.FullName; + + + + public PackageCrawler(string packageDirectoryPath) : this(new FileSystem(), packageDirectoryPath) + { + } + + public PackageCrawler(IFileSystem fileSystem, string packageDirectoryPath) + { + _fileSystem = fileSystem; + + _packageDirectoryInfo = GetPackageDirectory(packageDirectoryPath); + } + + + + private IDirectoryInfo GetPackageDirectory(string packageDirectoryPath) + { + var path = string.IsNullOrWhiteSpace(packageDirectoryPath) + ? _fileSystem.Directory.GetCurrentDirectory() + : packageDirectoryPath; + + var directoryInfo = _fileSystem.DirectoryInfo.FromDirectoryName(path); + + if (!directoryInfo.Exists) + { + throw new DirectoryNotFoundException($"Directory \"{directoryInfo.FullName}\" not found."); + } + + return directoryInfo; + } + + /// + /// Returns all installation packages (*.zip files) full file paths which were found in defined . + /// + /// Enumerable of full file paths. + internal IEnumerable GetPackagesFullPaths() + { + const string searchPattern = "*.zip"; + + return _packageDirectoryInfo.GetFiles(searchPattern, SearchOption.TopDirectoryOnly).Select(f => f.FullName); + + } + } +} diff --git a/DeployClient/Program.cs b/DeployClient/Program.cs index 8329565..0e8b688 100644 --- a/DeployClient/Program.cs +++ b/DeployClient/Program.cs @@ -1,8 +1,6 @@ using Cantarus.Libraries.Encryption; using System; -using System.Collections; using System.Collections.Generic; -using System.Configuration; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -11,11 +9,11 @@ namespace DeployClient { - class Program + internal static class Program { - internal static CommandLineOptions Options = new CommandLineOptions(); + internal static readonly CommandLineOptions Options = new CommandLineOptions(); - enum ExitCode : int + private enum ExitCode { Success = 0, Error = 1, @@ -24,7 +22,7 @@ enum ExitCode : int InstallFailure = 4 } - static async Task Main(string[] args) + private static async Task Main(string[] args) { try { @@ -55,29 +53,12 @@ static async Task Main(string[] args) // Output identifying module archives. WriteLine("Identifying module archives..."); - // Read zip files in current directory. - string currentDirectory = Directory.GetCurrentDirectory(); - List zipFiles = new List(Directory.GetFiles(currentDirectory, "*.zip")); - - // Is there something to do? - if (zipFiles.Count <= 0) - { - // No, exit. - WriteLine("No module archives found."); - WriteLine("Exiting."); - ReadLine(); - Environment.Exit((int)ExitCode.NoModulesFound); - } - - // Inform user of modules found. - WriteLine(string.Format("Found {0} module archives in {1}:", zipFiles.Count, currentDirectory)); - - foreach (string zipFile in zipFiles) - { - WriteLine(string.Format("\t{0}. {1}", zipFiles.IndexOf(zipFile) + 1, Path.GetFileName(zipFile))); - } - WriteLine(); + // Read zip files from packages directory if provided, otherwise from current directory + var packageCrawler = new PackageCrawler(Options.PackagesDirectoryPath); + var zipFiles = packageCrawler.GetPackagesFullPaths().ToArray(); + ValidateFoundPackages(zipFiles, packageCrawler.PackageDirectoryPath); + if (!Options.NoPrompt) { // Prompt to continue. @@ -269,6 +250,11 @@ private static void GetSettings(string[] args) { Options.EncryptionKey = Properties.Settings.Default.EncryptionKey; } + + if (string.IsNullOrWhiteSpace(Options.PackagesDirectoryPath)) + { + Options.PackagesDirectoryPath = Properties.Settings.Default.PackagesDirectory; + } } } @@ -352,6 +338,31 @@ private static void WriteLine(string message = "") Console.WriteLine(message); } + private static void ValidateFoundPackages(IEnumerable zipFiles, string directory) + { + var packages = zipFiles?.ToArray() ?? new string[0]; + + // Is there something to do? + if (!packages.Any()) + { + // No, exit. + WriteLine("No module archives found."); + WriteLine("Exiting."); + ReadLine(); + Environment.Exit((int)ExitCode.NoModulesFound); + } + + // Inform user of modules found. + WriteLine($"Found {packages.Length} module archives in {directory}:"); + + var fileCounter = 1; + foreach (var package in packages) + { + WriteLine($"\t{fileCounter++}. {Path.GetFileName(package)}"); + } + WriteLine(); + } + private static string ReadLine() { if (Options.IsSilent || Options.NoPrompt) diff --git a/DeployClient/Properties/AssemblyInfo.cs b/DeployClient/Properties/AssemblyInfo.cs index 84889ea..687f22b 100644 --- a/DeployClient/Properties/AssemblyInfo.cs +++ b/DeployClient/Properties/AssemblyInfo.cs @@ -34,3 +34,5 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("0.8.0.0")] [assembly: AssemblyFileVersion("0.8.0.0")] + +[assembly: InternalsVisibleTo("DeployClient.Tests")] diff --git a/DeployClient/packages.config b/DeployClient/packages.config index 9c3293f..5c2cb18 100644 --- a/DeployClient/packages.config +++ b/DeployClient/packages.config @@ -1,5 +1,6 @@  + \ No newline at end of file diff --git a/PolyDeploy.sln b/PolyDeploy.sln index 61c3a36..8870beb 100644 --- a/PolyDeploy.sln +++ b/PolyDeploy.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26403.7 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30128.74 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolyDeploy", "PolyDeploy\PolyDeploy.csproj", "{B15D41DD-2D1A-490C-977F-2F14E56447D5}" EndProject @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Encryption", "Encryption\En EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployClient", "DeployClient\DeployClient.csproj", "{B6122CCD-75F9-4DEF-8DAA-E11789D6D6D8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeployClient.Tests", "DeployClient.Tests\DeployClient.Tests.csproj", "{2F81EBF7-A395-4716-8B1C-23FE9EE8B7CD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Clients|Any CPU = Clients|Any CPU @@ -34,8 +36,17 @@ Global {B6122CCD-75F9-4DEF-8DAA-E11789D6D6D8}.Debug|Any CPU.Build.0 = Debug|Any CPU {B6122CCD-75F9-4DEF-8DAA-E11789D6D6D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {B6122CCD-75F9-4DEF-8DAA-E11789D6D6D8}.Release|Any CPU.Build.0 = Release|Any CPU + {2F81EBF7-A395-4716-8B1C-23FE9EE8B7CD}.Clients|Any CPU.ActiveCfg = Release|Any CPU + {2F81EBF7-A395-4716-8B1C-23FE9EE8B7CD}.Clients|Any CPU.Build.0 = Release|Any CPU + {2F81EBF7-A395-4716-8B1C-23FE9EE8B7CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F81EBF7-A395-4716-8B1C-23FE9EE8B7CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F81EBF7-A395-4716-8B1C-23FE9EE8B7CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F81EBF7-A395-4716-8B1C-23FE9EE8B7CD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9AE13660-40A1-4C85-B612-999111FB6DC7} + EndGlobalSection EndGlobal