From 2caa7852d352ca5246ba746b951ad895830d3a29 Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Fri, 2 Feb 2024 12:26:31 +0000 Subject: [PATCH] Convert all files to file-scoped name space --- src/Velopack.Packaging.Unix/AppImageTool.cs | 41 +- .../Commands/LinuxPackCommandRunner.cs | 139 ++- .../Commands/LinuxPackOptions.cs | 33 +- .../AuthenticodeTools.cs | 441 ++++---- src/Velopack.Packaging.Windows/CodeSign.cs | 449 ++++---- src/Velopack.Packaging.Windows/DotnetUtil.cs | 231 ++--- src/Velopack.Packaging.Windows/Rcedit.cs | 57 +- .../Abstractions/ICommand.cs | 9 +- .../Abstractions/IFancyConsole.cs | 15 +- .../Abstractions/IFancyConsoleProgress.cs | 9 +- .../Abstractions/IOutputOptions.cs | 9 +- .../Abstractions/IPackOptions.cs | 13 +- .../Abstractions/IPlatformOptions.cs | 9 +- src/Velopack.Packaging/BuildAssets.cs | 53 +- .../Commands/DeltaGenCommandRunner.cs | 43 +- .../Commands/DeltaGenOptions.cs | 15 +- .../Commands/DeltaPatchCommandRunner.cs | 65 +- .../Commands/DeltaPatchOptions.cs | 13 +- src/Velopack.Packaging/DeltaEmbedded.cs | 43 +- .../Exceptions/UserInfoException.cs | 35 +- .../VelopackAppVerificationException.cs | 19 +- src/Velopack.Packaging/Exe.cs | 139 ++- src/Velopack.Packaging/PackageBuilder.cs | 519 +++++----- src/Velopack.Packaging/ReleaseEntryHelper.cs | 459 ++++---- src/Velopack.Vpk/Commands/DeltaGenCommand.cs | 67 +- .../Commands/DeltaPatchCommand.cs | 53 +- src/Velopack.Vpk/Commands/LinuxPackCommand.cs | 157 ++- src/Velopack.Vpk/Commands/LongHelpCommand.cs | 517 +++++---- src/Velopack.Vpk/Commands/_OutputCommand.cs | 57 +- src/Velopack.Vpk/Commands/_PlatformCommand.cs | 35 +- src/Velopack.Vpk/Logging/BasicConsole.cs | 91 +- .../Logging/DefaultPromptValueFactory.cs | 7 +- src/Velopack.Vpk/Logging/SpectreConsole.cs | 231 ++--- test/LegacyTestApp/Program.cs | 179 ++-- test/LegacyTestApp/SquirrelLogger.cs | 71 +- .../DotnetUtilTests.cs | 165 ++- .../GithubDeploymentTests.cs | 385 ++++--- test/Velopack.Packaging.Tests/Init.cs | 15 +- .../S3DeploymentTests.cs | 135 ++- test/Velopack.Packaging.Tests/TestApp.cs | 131 ++- .../OldSquirrel/ReleaseEntry.cs | 539 +++++----- .../OldSquirrel/ReleaseExtensions.cs | 31 +- .../OldSquirrel/SemanticVersion.cs | 567 +++++----- test/Velopack.Tests/OldSquirrel/Utility.cs | 21 +- test/Velopack.Tests/ReleaseEntryTests.cs | 979 +++++++++--------- test/Velopack.Tests/RuntimeInfoTests.cs | 59 +- test/Velopack.Tests/RuntimeTests.cs | 187 ++-- test/Velopack.Tests/ShortcutTests.cs | 81 +- test/Velopack.Tests/SimpleJsonTests.cs | 247 +++-- .../TestHelpers/AssertExtensions.cs | 273 +++-- .../TestHelpers/ExposedClass.cs | 211 ++-- .../TestHelpers/ExposedObject.cs | 306 +++--- .../TestHelpers/ExposedObjectHelper.cs | 143 ++- .../TestHelpers/FakeDownloader.cs | 61 +- .../TestHelpers/FakeFixtureRepository.cs | 179 ++-- .../TestHelpers/StaticHttpServer.cs | 143 ++- test/Velopack.Tests/UpdateManagerTests.cs | 685 ++++++------ test/Velopack.Tests/UtilityTests.cs | 455 ++++---- test/Velopack.Tests/ZipPackageTests.cs | 131 ++- 59 files changed, 5196 insertions(+), 5256 deletions(-) diff --git a/src/Velopack.Packaging.Unix/AppImageTool.cs b/src/Velopack.Packaging.Unix/AppImageTool.cs index 1ee7d555..2a7878c4 100644 --- a/src/Velopack.Packaging.Unix/AppImageTool.cs +++ b/src/Velopack.Packaging.Unix/AppImageTool.cs @@ -7,31 +7,30 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Velopack.Packaging.Exceptions; -namespace Velopack.Packaging.Unix +namespace Velopack.Packaging.Unix; + +public class AppImageTool { - public class AppImageTool + [SupportedOSPlatform("linux")] + public static void CreateLinuxAppImage(string appDir, string outputFile, RuntimeCpu machine, ILogger logger) { - [SupportedOSPlatform("linux")] - public static void CreateLinuxAppImage(string appDir, string outputFile, RuntimeCpu machine, ILogger logger) - { - var tool = HelperFile.AppImageToolX64; + var tool = HelperFile.AppImageToolX64; - string arch = machine switch { - RuntimeCpu.x86 => "i386", - RuntimeCpu.x64 => "x86_64", - RuntimeCpu.arm64 => "arm_aarch64", - _ => throw new ArgumentOutOfRangeException(nameof(machine), machine, null) - }; + string arch = machine switch { + RuntimeCpu.x86 => "i386", + RuntimeCpu.x64 => "x86_64", + RuntimeCpu.arm64 => "arm_aarch64", + _ => throw new ArgumentOutOfRangeException(nameof(machine), machine, null) + }; - var envVar = new Dictionary() { - { "ARCH", arch } - }; - - logger.Info("About to create .AppImage for architecture: " + arch); + var envVar = new Dictionary() { + { "ARCH", arch } + }; + + logger.Info("About to create .AppImage for architecture: " + arch); - Chmod.ChmodFileAsExecutable(tool); - Exe.InvokeAndThrowIfNonZero(tool, new[] { appDir, outputFile }, null, envVar); - Chmod.ChmodFileAsExecutable(outputFile); - } + Chmod.ChmodFileAsExecutable(tool); + Exe.InvokeAndThrowIfNonZero(tool, new[] { appDir, outputFile }, null, envVar); + Chmod.ChmodFileAsExecutable(outputFile); } } diff --git a/src/Velopack.Packaging.Unix/Commands/LinuxPackCommandRunner.cs b/src/Velopack.Packaging.Unix/Commands/LinuxPackCommandRunner.cs index 56214fd8..82a34940 100644 --- a/src/Velopack.Packaging.Unix/Commands/LinuxPackCommandRunner.cs +++ b/src/Velopack.Packaging.Unix/Commands/LinuxPackCommandRunner.cs @@ -8,44 +8,44 @@ using ELFSharp.ELF; using Microsoft.Extensions.Logging; using Velopack.Packaging.Abstractions; -namespace Velopack.Packaging.Unix.Commands +namespace Velopack.Packaging.Unix.Commands; + +[SupportedOSPlatform("linux")] +public class LinuxPackCommandRunner : PackageBuilder { - [SupportedOSPlatform("linux")] - public class LinuxPackCommandRunner : PackageBuilder + protected string PortablePackagePath { get; set; } + + public LinuxPackCommandRunner(ILogger logger, IFancyConsole console) + : base(RuntimeOs.Linux, logger, console) { - protected string PortablePackagePath { get; set; } + } - public LinuxPackCommandRunner(ILogger logger, IFancyConsole console) - : base(RuntimeOs.Linux, logger, console) - { - } + protected override Task PreprocessPackDir(Action progress, string packDir) + { + var dir = TempDir.CreateSubdirectory("PreprocessPackDir.AppDir"); + var bin = dir.CreateSubdirectory("usr").CreateSubdirectory("bin"); - protected override Task PreprocessPackDir(Action progress, string packDir) - { - var dir = TempDir.CreateSubdirectory("PreprocessPackDir.AppDir"); - var bin = dir.CreateSubdirectory("usr").CreateSubdirectory("bin"); - - if (Options.PackIsAppDir) { - Log.Info("Using provided .AppDir, will skip building new one."); - CopyFiles(new DirectoryInfo(Options.PackDirectory), dir, progress, true); - } else { - Log.Info("Building new .AppDir"); - var appRunPath = Path.Combine(dir.FullName, "AppRun"); - File.WriteAllText(appRunPath, """ + if (Options.PackIsAppDir) { + Log.Info("Using provided .AppDir, will skip building new one."); + CopyFiles(new DirectoryInfo(Options.PackDirectory), dir, progress, true); + } else { + Log.Info("Building new .AppDir"); + var appRunPath = Path.Combine(dir.FullName, "AppRun"); + File.WriteAllText(appRunPath, """ #!/bin/sh HERE="$(dirname "$(readlink -f "${0}")")" export PATH="${HERE}"/usr/bin/:"${PATH}" EXEC=$(grep -e '^Exec=.*' "${HERE}"/*.desktop | head -n 1 | cut -d "=" -f 2 | cut -d " " -f 1) exec "${EXEC}" $@ """); - Chmod.ChmodFileAsExecutable(appRunPath); + Chmod.ChmodFileAsExecutable(appRunPath); - var mainExeName = Options.EntryExecutableName ?? Options.PackId; - var mainExePath = Path.Combine(packDir, mainExeName); - if (!File.Exists(mainExePath)) - throw new Exception($"Could not find main executable at '{mainExePath}'. Please specify with --exeName."); + var mainExeName = Options.EntryExecutableName ?? Options.PackId; + var mainExePath = Path.Combine(packDir, mainExeName); + if (!File.Exists(mainExePath)) + throw new Exception($"Could not find main executable at '{mainExePath}'. Please specify with --exeName."); - File.WriteAllText(Path.Combine(dir.FullName, Options.PackId + ".desktop"), $""" + File.WriteAllText(Path.Combine(dir.FullName, Options.PackId + ".desktop"), $""" [Desktop Entry] Type=Application Name={Options.PackTitle ?? Options.PackId} @@ -56,56 +56,55 @@ Path=~ Categories=Development; """); - // copy existing app files - CopyFiles(new DirectoryInfo(packDir), bin, progress, true); - // app icon - File.Copy(Options.Icon, Path.Combine(dir.FullName, Options.PackId + Path.GetExtension(Options.Icon)), true); - } - - // velopack required files - File.WriteAllText(Path.Combine(bin.FullName, "sq.version"), GenerateNuspecContent()); - File.Copy(HelperFile.GetUpdatePath(), Path.Combine(bin.FullName, "UpdateNix"), true); - progress(100); - return Task.FromResult(dir.FullName); + // copy existing app files + CopyFiles(new DirectoryInfo(packDir), bin, progress, true); + // app icon + File.Copy(Options.Icon, Path.Combine(dir.FullName, Options.PackId + Path.GetExtension(Options.Icon)), true); } - protected override Task CreatePortablePackage(Action progress, string packDir, string outputPath) - { - progress(-1); - var machine = Options.TargetRuntime.HasArchitecture - ? Options.TargetRuntime.Architecture - : GetMachineForBinary(MainExePath); - AppImageTool.CreateLinuxAppImage(packDir, outputPath, machine, Log); - PortablePackagePath = outputPath; - progress(100); - return Task.CompletedTask; - } + // velopack required files + File.WriteAllText(Path.Combine(bin.FullName, "sq.version"), GenerateNuspecContent()); + File.Copy(HelperFile.GetUpdatePath(), Path.Combine(bin.FullName, "UpdateNix"), true); + progress(100); + return Task.FromResult(dir.FullName); + } - protected virtual RuntimeCpu GetMachineForBinary(string path) - { - var elf = ELFReader.Load(path); + protected override Task CreatePortablePackage(Action progress, string packDir, string outputPath) + { + progress(-1); + var machine = Options.TargetRuntime.HasArchitecture + ? Options.TargetRuntime.Architecture + : GetMachineForBinary(MainExePath); + AppImageTool.CreateLinuxAppImage(packDir, outputPath, machine, Log); + PortablePackagePath = outputPath; + progress(100); + return Task.CompletedTask; + } - var machine = elf.Machine switch { - Machine.AArch64 => RuntimeCpu.arm64, - Machine.AMD64 => RuntimeCpu.x64, - Machine.Intel386 => RuntimeCpu.x86, - _ => throw new Exception($"Unsupported ELF machine type '{elf.Machine}'.") - }; + protected virtual RuntimeCpu GetMachineForBinary(string path) + { + var elf = ELFReader.Load(path); - return machine; - } + var machine = elf.Machine switch { + Machine.AArch64 => RuntimeCpu.arm64, + Machine.AMD64 => RuntimeCpu.x64, + Machine.Intel386 => RuntimeCpu.x86, + _ => throw new Exception($"Unsupported ELF machine type '{elf.Machine}'.") + }; - protected override Task CreateReleasePackage(Action progress, string packDir, string outputPath) - { - var dir = TempDir.CreateSubdirectory("CreateReleasePackage.Linux"); - File.Copy(PortablePackagePath, Path.Combine(dir.FullName, Options.PackId + ".AppImage"), true); - return base.CreateReleasePackage(progress, dir.FullName, outputPath); - } + return machine; + } - protected override Task CreateDeltaPackage(Action progress, string releasePkg, string prevReleasePkg, string outputPkg, DeltaMode mode) - { - progress(-1); // there is only one "file", so progress will not work - return base.CreateDeltaPackage(progress, releasePkg, prevReleasePkg, outputPkg, mode); - } + protected override Task CreateReleasePackage(Action progress, string packDir, string outputPath) + { + var dir = TempDir.CreateSubdirectory("CreateReleasePackage.Linux"); + File.Copy(PortablePackagePath, Path.Combine(dir.FullName, Options.PackId + ".AppImage"), true); + return base.CreateReleasePackage(progress, dir.FullName, outputPath); + } + + protected override Task CreateDeltaPackage(Action progress, string releasePkg, string prevReleasePkg, string outputPkg, DeltaMode mode) + { + progress(-1); // there is only one "file", so progress will not work + return base.CreateDeltaPackage(progress, releasePkg, prevReleasePkg, outputPkg, mode); } } diff --git a/src/Velopack.Packaging.Unix/Commands/LinuxPackOptions.cs b/src/Velopack.Packaging.Unix/Commands/LinuxPackOptions.cs index 14357d77..ec9a7f17 100644 --- a/src/Velopack.Packaging.Unix/Commands/LinuxPackOptions.cs +++ b/src/Velopack.Packaging.Unix/Commands/LinuxPackOptions.cs @@ -5,34 +5,33 @@ using System.Text; using System.Threading.Tasks; using Velopack.Packaging.Abstractions; -namespace Velopack.Packaging.Unix.Commands +namespace Velopack.Packaging.Unix.Commands; + +public class LinuxPackOptions : IPackOptions { - public class LinuxPackOptions : IPackOptions - { - public DirectoryInfo ReleaseDir { get; set; } + public DirectoryInfo ReleaseDir { get; set; } - public string PackId { get; set; } + public string PackId { get; set; } - public string PackVersion { get; set; } + public string PackVersion { get; set; } - public string PackDirectory { get; set; } + public string PackDirectory { get; set; } - public string PackAuthors { get; set; } + public string PackAuthors { get; set; } - public string PackTitle { get; set; } + public string PackTitle { get; set; } - public string EntryExecutableName { get; set; } + public string EntryExecutableName { get; set; } - public string Icon { get; set; } + public string Icon { get; set; } - public RID TargetRuntime { get; set; } + public RID TargetRuntime { get; set; } - public string ReleaseNotes { get; set; } + public string ReleaseNotes { get; set; } - public DeltaMode DeltaMode { get; set; } = DeltaMode.BestSpeed; + public DeltaMode DeltaMode { get; set; } = DeltaMode.BestSpeed; - public string Channel { get; set; } + public string Channel { get; set; } - public bool PackIsAppDir { get; set; } - } + public bool PackIsAppDir { get; set; } } diff --git a/src/Velopack.Packaging.Windows/AuthenticodeTools.cs b/src/Velopack.Packaging.Windows/AuthenticodeTools.cs index 9f73245e..0e1aff68 100644 --- a/src/Velopack.Packaging.Windows/AuthenticodeTools.cs +++ b/src/Velopack.Packaging.Windows/AuthenticodeTools.cs @@ -2,241 +2,240 @@ using System.Runtime.InteropServices; using System.Runtime.Versioning; -namespace Velopack.Packaging.Windows +namespace Velopack.Packaging.Windows; + +[SupportedOSPlatform("windows")] +[ExcludeFromCodeCoverage] +public static class AuthenticodeTools { - [SupportedOSPlatform("windows")] - [ExcludeFromCodeCoverage] - public static class AuthenticodeTools + [DllImport("Wintrust.dll", PreserveSig = true, SetLastError = false)] + static extern uint WinVerifyTrust(IntPtr hWnd, IntPtr pgActionID, IntPtr pWinTrustData); + + static uint winVerifyTrust(string fileName) { - [DllImport("Wintrust.dll", PreserveSig = true, SetLastError = false)] - static extern uint WinVerifyTrust(IntPtr hWnd, IntPtr pgActionID, IntPtr pWinTrustData); + Guid wintrust_action_generic_verify_v2 = new Guid("{00AAC56B-CD44-11d0-8CC2-00C04FC295EE}"); - static uint winVerifyTrust(string fileName) - { - Guid wintrust_action_generic_verify_v2 = new Guid("{00AAC56B-CD44-11d0-8CC2-00C04FC295EE}"); + uint result = 0; + using (WINTRUST_FILE_INFO fileInfo = new WINTRUST_FILE_INFO(fileName, Guid.Empty)) + using (UnmanagedPointer guidPtr = new UnmanagedPointer(Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid))), AllocMethod.HGlobal)) + using (UnmanagedPointer wvtDataPtr = new UnmanagedPointer(Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WINTRUST_DATA))), AllocMethod.HGlobal)) { + WINTRUST_DATA data = new WINTRUST_DATA(fileInfo); + IntPtr pGuid = guidPtr; + IntPtr pData = wvtDataPtr; - uint result = 0; - using (WINTRUST_FILE_INFO fileInfo = new WINTRUST_FILE_INFO(fileName, Guid.Empty)) - using (UnmanagedPointer guidPtr = new UnmanagedPointer(Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid))), AllocMethod.HGlobal)) - using (UnmanagedPointer wvtDataPtr = new UnmanagedPointer(Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WINTRUST_DATA))), AllocMethod.HGlobal)) { - WINTRUST_DATA data = new WINTRUST_DATA(fileInfo); - IntPtr pGuid = guidPtr; - IntPtr pData = wvtDataPtr; - - Marshal.StructureToPtr(wintrust_action_generic_verify_v2, pGuid, true); - Marshal.StructureToPtr(data, pData, true); - - result = WinVerifyTrust(IntPtr.Zero, pGuid, pData); - } - return result; + Marshal.StructureToPtr(wintrust_action_generic_verify_v2, pGuid, true); + Marshal.StructureToPtr(data, pData, true); + result = WinVerifyTrust(IntPtr.Zero, pGuid, pData); } - public static bool IsTrusted(string fileName) - { - return winVerifyTrust(fileName) == 0; + return result; + + } + public static bool IsTrusted(string fileName) + { + return winVerifyTrust(fileName) == 0; + } +} + +[ExcludeFromCodeCoverage] +internal struct WINTRUST_FILE_INFO : IDisposable +{ + public WINTRUST_FILE_INFO(string fileName, Guid subject) + { + + cbStruct = (uint) Marshal.SizeOf(typeof(WINTRUST_FILE_INFO)); + pcwszFilePath = fileName; + + if (subject != Guid.Empty) { + pgKnownSubject = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid))); + Marshal.StructureToPtr(subject, pgKnownSubject, true); + } else { + pgKnownSubject = IntPtr.Zero; } + + hFile = IntPtr.Zero; + } - [ExcludeFromCodeCoverage] - internal struct WINTRUST_FILE_INFO : IDisposable + public uint cbStruct; + + [MarshalAs(UnmanagedType.LPTStr)] + public string pcwszFilePath; + + public IntPtr hFile; + public IntPtr pgKnownSubject; + + public void Dispose() { - public WINTRUST_FILE_INFO(string fileName, Guid subject) - { - - cbStruct = (uint) Marshal.SizeOf(typeof(WINTRUST_FILE_INFO)); - pcwszFilePath = fileName; - - if (subject != Guid.Empty) { - pgKnownSubject = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid))); - Marshal.StructureToPtr(subject, pgKnownSubject, true); - } else { - pgKnownSubject = IntPtr.Zero; - } - - hFile = IntPtr.Zero; - - } - - public uint cbStruct; - - [MarshalAs(UnmanagedType.LPTStr)] - public string pcwszFilePath; - - public IntPtr hFile; - public IntPtr pgKnownSubject; - - public void Dispose() - { - Dispose(true); - } - - void Dispose(bool disposing) - { - if (pgKnownSubject != IntPtr.Zero) { - Marshal.DestroyStructure(this.pgKnownSubject, typeof(Guid)); - Marshal.FreeHGlobal(this.pgKnownSubject); - } - } + Dispose(true); } - enum AllocMethod + void Dispose(bool disposing) { - HGlobal, CoTaskMem - }; - - enum UnionChoice - { - File = 1, - Catalog, - Blob, - Signer, - Cert - }; - - enum UiChoice - { - All = 1, - NoUI, - NoBad, - NoGood - }; - enum RevocationCheckFlags - { - None = 0, - WholeChain - }; - enum StateAction - { - Ignore = 0, - Verify, - Close, - AutoCache, - AutoCacheFlush - }; - enum TrustProviderFlags - { - UseIE4Trust = 1, - NoIE4Chain = 2, - NoPolicyUsage = 4, - RevocationCheckNone = 16, - RevocationCheckEndCert = 32, - RevocationCheckChain = 64, - RecovationCheckChainExcludeRoot = 128, - Safer = 256, - HashOnly = 512, - UseDefaultOSVerCheck = 1024, - LifetimeSigning = 2048 - }; - enum UIContext - { - Execute = 0, - Install - }; - - [StructLayout(LayoutKind.Sequential)] - - [ExcludeFromCodeCoverage] - internal struct WINTRUST_DATA : IDisposable - { - public WINTRUST_DATA(WINTRUST_FILE_INFO fileInfo) - { - this.cbStruct = (uint) Marshal.SizeOf(typeof(WINTRUST_DATA)); - pInfoStruct = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WINTRUST_FILE_INFO))); - - Marshal.StructureToPtr(fileInfo, pInfoStruct, false); - - this.dwUnionChoice = UnionChoice.File; - - pPolicyCallbackData = IntPtr.Zero; - pSIPCallbackData = IntPtr.Zero; - dwUIChoice = UiChoice.NoUI; - fdwRevocationChecks = RevocationCheckFlags.None; - dwStateAction = StateAction.Ignore; - hWVTStateData = IntPtr.Zero; - pwszURLReference = IntPtr.Zero; - dwProvFlags = TrustProviderFlags.Safer; - dwUIContext = UIContext.Execute; - } - - public uint cbStruct; - public IntPtr pPolicyCallbackData; - public IntPtr pSIPCallbackData; - public UiChoice dwUIChoice; - public RevocationCheckFlags fdwRevocationChecks; - public UnionChoice dwUnionChoice; - public IntPtr pInfoStruct; - public StateAction dwStateAction; - public IntPtr hWVTStateData; - public TrustProviderFlags dwProvFlags; - public UIContext dwUIContext; - - IntPtr pwszURLReference; - - public void Dispose() - { - Dispose(true); - } - - - - void Dispose(bool disposing) - { - if (dwUnionChoice == UnionChoice.File) { - WINTRUST_FILE_INFO info = new WINTRUST_FILE_INFO(); - Marshal.PtrToStructure(pInfoStruct, info); - - info.Dispose(); - - Marshal.DestroyStructure(pInfoStruct, typeof(WINTRUST_FILE_INFO)); - } - - Marshal.FreeHGlobal(pInfoStruct); - } - } - - [ExcludeFromCodeCoverage] - internal sealed class UnmanagedPointer : IDisposable - { - IntPtr m_ptr; - AllocMethod m_meth; - - internal UnmanagedPointer(IntPtr ptr, AllocMethod method) - { - m_meth = method; - m_ptr = ptr; - } - - ~UnmanagedPointer() - { - Dispose(false); - } - - void Dispose(bool disposing) - { - if (m_ptr != IntPtr.Zero) { - if (m_meth == AllocMethod.HGlobal) { - Marshal.FreeHGlobal(m_ptr); - } else if (m_meth == AllocMethod.CoTaskMem) { - Marshal.FreeCoTaskMem(m_ptr); - } - - m_ptr = IntPtr.Zero; - } - - if (disposing) { - GC.SuppressFinalize(this); - } - } - - public void Dispose() - { - Dispose(true); - } - - public static implicit operator IntPtr(UnmanagedPointer ptr) - { - return ptr.m_ptr; + if (pgKnownSubject != IntPtr.Zero) { + Marshal.DestroyStructure(this.pgKnownSubject, typeof(Guid)); + Marshal.FreeHGlobal(this.pgKnownSubject); } } } +enum AllocMethod +{ + HGlobal, CoTaskMem +}; + +enum UnionChoice +{ + File = 1, + Catalog, + Blob, + Signer, + Cert +}; + +enum UiChoice +{ + All = 1, + NoUI, + NoBad, + NoGood +}; +enum RevocationCheckFlags +{ + None = 0, + WholeChain +}; +enum StateAction +{ + Ignore = 0, + Verify, + Close, + AutoCache, + AutoCacheFlush +}; +enum TrustProviderFlags +{ + UseIE4Trust = 1, + NoIE4Chain = 2, + NoPolicyUsage = 4, + RevocationCheckNone = 16, + RevocationCheckEndCert = 32, + RevocationCheckChain = 64, + RecovationCheckChainExcludeRoot = 128, + Safer = 256, + HashOnly = 512, + UseDefaultOSVerCheck = 1024, + LifetimeSigning = 2048 +}; +enum UIContext +{ + Execute = 0, + Install +}; + +[StructLayout(LayoutKind.Sequential)] + +[ExcludeFromCodeCoverage] +internal struct WINTRUST_DATA : IDisposable +{ + public WINTRUST_DATA(WINTRUST_FILE_INFO fileInfo) + { + this.cbStruct = (uint) Marshal.SizeOf(typeof(WINTRUST_DATA)); + pInfoStruct = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WINTRUST_FILE_INFO))); + + Marshal.StructureToPtr(fileInfo, pInfoStruct, false); + + this.dwUnionChoice = UnionChoice.File; + + pPolicyCallbackData = IntPtr.Zero; + pSIPCallbackData = IntPtr.Zero; + dwUIChoice = UiChoice.NoUI; + fdwRevocationChecks = RevocationCheckFlags.None; + dwStateAction = StateAction.Ignore; + hWVTStateData = IntPtr.Zero; + pwszURLReference = IntPtr.Zero; + dwProvFlags = TrustProviderFlags.Safer; + dwUIContext = UIContext.Execute; + } + + public uint cbStruct; + public IntPtr pPolicyCallbackData; + public IntPtr pSIPCallbackData; + public UiChoice dwUIChoice; + public RevocationCheckFlags fdwRevocationChecks; + public UnionChoice dwUnionChoice; + public IntPtr pInfoStruct; + public StateAction dwStateAction; + public IntPtr hWVTStateData; + public TrustProviderFlags dwProvFlags; + public UIContext dwUIContext; + + IntPtr pwszURLReference; + + public void Dispose() + { + Dispose(true); + } + + + + void Dispose(bool disposing) + { + if (dwUnionChoice == UnionChoice.File) { + WINTRUST_FILE_INFO info = new WINTRUST_FILE_INFO(); + Marshal.PtrToStructure(pInfoStruct, info); + + info.Dispose(); + + Marshal.DestroyStructure(pInfoStruct, typeof(WINTRUST_FILE_INFO)); + } + + Marshal.FreeHGlobal(pInfoStruct); + } +} + +[ExcludeFromCodeCoverage] +internal sealed class UnmanagedPointer : IDisposable +{ + IntPtr m_ptr; + AllocMethod m_meth; + + internal UnmanagedPointer(IntPtr ptr, AllocMethod method) + { + m_meth = method; + m_ptr = ptr; + } + + ~UnmanagedPointer() + { + Dispose(false); + } + + void Dispose(bool disposing) + { + if (m_ptr != IntPtr.Zero) { + if (m_meth == AllocMethod.HGlobal) { + Marshal.FreeHGlobal(m_ptr); + } else if (m_meth == AllocMethod.CoTaskMem) { + Marshal.FreeCoTaskMem(m_ptr); + } + + m_ptr = IntPtr.Zero; + } + + if (disposing) { + GC.SuppressFinalize(this); + } + } + + public void Dispose() + { + Dispose(true); + } + + public static implicit operator IntPtr(UnmanagedPointer ptr) + { + return ptr.m_ptr; + } +} + diff --git a/src/Velopack.Packaging.Windows/CodeSign.cs b/src/Velopack.Packaging.Windows/CodeSign.cs index 0851baa3..bd1eaff0 100644 --- a/src/Velopack.Packaging.Windows/CodeSign.cs +++ b/src/Velopack.Packaging.Windows/CodeSign.cs @@ -4,237 +4,236 @@ using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Velopack.Packaging.Exceptions; -namespace Velopack.Packaging.Windows -{ - [SupportedOSPlatform("windows")] - public class CodeSign - { - public ILogger Log { get; } +namespace Velopack.Packaging.Windows; - public CodeSign(ILogger logger) - { - Log = logger; +[SupportedOSPlatform("windows")] +public class CodeSign +{ + public ILogger Log { get; } + + public CodeSign(ILogger logger) + { + Log = logger; + } + + private bool CheckIsAlreadySigned(string filePath) + { + if (String.IsNullOrWhiteSpace(filePath)) return true; + + if (!File.Exists(filePath)) { + Log.Warn($"Cannot sign '{filePath}', file does not exist."); + return true; } - private bool CheckIsAlreadySigned(string filePath) - { - if (String.IsNullOrWhiteSpace(filePath)) return true; - - if (!File.Exists(filePath)) { - Log.Warn($"Cannot sign '{filePath}', file does not exist."); + try { + if (AuthenticodeTools.IsTrusted(filePath)) { + Log.Debug($"'{filePath}' is already signed, skipping..."); return true; } - - try { - if (AuthenticodeTools.IsTrusted(filePath)) { - Log.Debug($"'{filePath}' is already signed, skipping..."); - return true; - } - } catch (Exception ex) { - Log.Error(ex, "Failed to determine signing status for " + filePath); - } - - return false; + } catch (Exception ex) { + Log.Error(ex, "Failed to determine signing status for " + filePath); } - public void SignPEFilesWithSignTool(string rootDir, string[] filePaths, string signArguments, int parallelism, Action progress) - { - Queue pendingSign = new Queue(); - - foreach (var f in filePaths) { - if (!CheckIsAlreadySigned(f)) { - // try to find the path relative to rootDir - if (String.IsNullOrEmpty(rootDir)) { - pendingSign.Enqueue(f); - } else { - var partialPath = Utility.NormalizePath(f).Substring(Utility.NormalizePath(rootDir).Length).Trim('/', '\\'); - pendingSign.Enqueue(partialPath); - } - } else { - Log.Debug($"'{f}' is already signed, and will not be signed again."); - } - } - - if (filePaths.Length != pendingSign.Count) { - var diff = filePaths.Length - pendingSign.Count; - Log.Info($"{pendingSign.Count} files will be signed, {diff} will be skipped because they are already signed."); - } - - // here we invoke signtool.exe with 'cmd.exe /C' and redirect output to a file, because something - // about how the dotnet tool host works prevents signtool from being able to open a token password - // prompt, meaning signing fails for those with an HSM. - using var _1 = Utility.GetTempFileName(out var signLogFile); - - var totalToSign = pendingSign.Count; - - do { - List filesToSign = new List(); - for (int i = Math.Min(pendingSign.Count, parallelism); i > 0; i--) { - filesToSign.Add(pendingSign.Dequeue()); - } - - var filesToSignStr = String.Join(" ", filesToSign.Select(f => $"\"{f}\"")); - var command = $"/S /C \"\"{HelperFile.SignToolPath}\" sign {signArguments} {filesToSignStr} >> \"{signLogFile}\" 2>&1\""; - - var psi = new ProcessStartInfo { - FileName = "cmd.exe", - Arguments = command, - UseShellExecute = false, - WorkingDirectory = rootDir, - CreateNoWindow = true - }; - - var process = Process.Start(psi); - process.WaitForExit(); - - if (process.ExitCode != 0) { - var cmdWithPasswordHidden = "cmd.exe " + new Regex(@"\/p\s+?[^\s]+").Replace(command, "/p ********"); - Log.Debug($"Signing command failed: {cmdWithPasswordHidden}"); - var output = File.Exists(signLogFile) ? File.ReadAllText(signLogFile).Trim() : "No output file was created."; - throw new UserInfoException( - $"Signing command failed. Specify --verbose argument to print signing command.\n" + - $"Output was:" + Environment.NewLine + output); - } - - int processed = totalToSign - pendingSign.Count; - Log.Info($"Code-signed {processed}/{totalToSign} files"); - progress((int) ((double) processed / totalToSign * 100)); - } while (pendingSign.Count > 0); - - Log.Info("SignTool Output: " + Environment.NewLine + File.ReadAllText(signLogFile).Trim()); - } - - public void SignPEFileWithTemplate(string filePath, string signTemplate) - { - if (VelopackRuntimeInfo.IsWindows && CheckIsAlreadySigned(filePath)) { - Log.Debug($"'{filePath}' is already signed, and will not be signed again."); - return; - } - - var command = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\""); - - var result = Exe.InvokeProcess(command, null, null); - if (result.ExitCode != 0) { - var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********"); - Log.Debug($"Signing command failed: {cmdWithPasswordHidden}"); - throw new Exception( - $"Signing command failed. Specify --verbose argument to print signing command.\n\n" + - $"Output was:\n" + result.StdOutput); - } - - Log.Info("Sign successful: " + result.StdOutput); - } - - //private static ProcessStartInfo CreateProcessStartInfo(string fileName, string arguments, string workingDirectory = "") - //{ - // var psi = new ProcessStartInfo(fileName, arguments); - // psi.UseShellExecute = false; - // psi.WindowStyle = ProcessWindowStyle.Hidden; - // psi.ErrorDialog = false; - // psi.CreateNoWindow = true; - // psi.RedirectStandardOutput = true; - // psi.RedirectStandardError = true; - // psi.WorkingDirectory = workingDirectory; - // return psi; - //} - - //private void SignPEFile(string filePath, string signParams, string signTemplate) - //{ - // try { - // if (AuthenticodeTools.IsTrusted(filePath)) { - // Log.Debug($"'{filePath}' is already signed, skipping..."); - // return; - // } - // } catch (Exception ex) { - // Log.Error(ex, "Failed to determine signing status for " + filePath); - // } - - // string cmd; - // ProcessStartInfo psi; - // if (!String.IsNullOrEmpty(signParams)) { - // // use embedded signtool.exe with provided parameters - // cmd = $"sign {signParams} \"{filePath}\""; - // psi = CreateProcessStartInfo(HelperFile.SignToolPath, cmd); - // cmd = "signtool.exe " + cmd; - // } else if (!String.IsNullOrEmpty(signTemplate)) { - // // escape custom sign command and pass it to cmd.exe - // cmd = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\""); - // psi = CreateProcessStartInfo("cmd", $"/c {EscapeCmdExeMetachars(cmd)}"); - // } else { - // Log.Debug($"{filePath} was not signed. (skipped; no signing parameters)"); - // return; - // } - - // var processResult = InvokeProcessUnsafeAsync(psi, CancellationToken.None) - // .ConfigureAwait(false).GetAwaiter().GetResult(); - - // if (processResult.ExitCode != 0) { - // var cmdWithPasswordHidden = new Regex(@"/p\s+\w+").Replace(cmd, "/p ********"); - // throw new Exception("Signing command failed: \n > " + cmdWithPasswordHidden + "\n" + processResult.StdOutput); - // } else { - // Log.Info("Sign successful: " + processResult.StdOutput); - // } - //} - - //private static string EscapeCmdExeMetachars(string command) - //{ - // var result = new StringBuilder(); - // foreach (var ch in command) { - // switch (ch) { - // case '(': - // case ')': - // case '%': - // case '!': - // case '^': - // case '"': - // case '<': - // case '>': - // case '&': - // case '|': - // result.Append('^'); - // break; - // } - // result.Append(ch); - // } - // return result.ToString(); - //} - - //private class ProcessResult - //{ - // public int ExitCode { get; set; } - // public string StdOutput { get; set; } - - // public ProcessResult(int exitCode, string stdOutput) - // { - // ExitCode = exitCode; - // StdOutput = stdOutput; - // } - //} - - //private static async Task InvokeProcessUnsafeAsync(ProcessStartInfo psi, CancellationToken ct) - //{ - // var pi = Process.Start(psi); - // await Task.Run(() => { - // while (!ct.IsCancellationRequested) { - // if (pi.WaitForExit(2000)) return; - // } - - // if (ct.IsCancellationRequested) { - // pi.Kill(); - // ct.ThrowIfCancellationRequested(); - // } - // }).ConfigureAwait(false); - - // string textResult = await pi.StandardOutput.ReadToEndAsync().ConfigureAwait(false); - // if (String.IsNullOrWhiteSpace(textResult) || pi.ExitCode != 0) { - // textResult = (textResult ?? "") + "\n" + await pi.StandardError.ReadToEndAsync().ConfigureAwait(false); - - // if (String.IsNullOrWhiteSpace(textResult)) { - // textResult = String.Empty; - // } - // } - - // return new ProcessResult(pi.ExitCode, textResult.Trim()); - //} + return false; } + + public void SignPEFilesWithSignTool(string rootDir, string[] filePaths, string signArguments, int parallelism, Action progress) + { + Queue pendingSign = new Queue(); + + foreach (var f in filePaths) { + if (!CheckIsAlreadySigned(f)) { + // try to find the path relative to rootDir + if (String.IsNullOrEmpty(rootDir)) { + pendingSign.Enqueue(f); + } else { + var partialPath = Utility.NormalizePath(f).Substring(Utility.NormalizePath(rootDir).Length).Trim('/', '\\'); + pendingSign.Enqueue(partialPath); + } + } else { + Log.Debug($"'{f}' is already signed, and will not be signed again."); + } + } + + if (filePaths.Length != pendingSign.Count) { + var diff = filePaths.Length - pendingSign.Count; + Log.Info($"{pendingSign.Count} files will be signed, {diff} will be skipped because they are already signed."); + } + + // here we invoke signtool.exe with 'cmd.exe /C' and redirect output to a file, because something + // about how the dotnet tool host works prevents signtool from being able to open a token password + // prompt, meaning signing fails for those with an HSM. + using var _1 = Utility.GetTempFileName(out var signLogFile); + + var totalToSign = pendingSign.Count; + + do { + List filesToSign = new List(); + for (int i = Math.Min(pendingSign.Count, parallelism); i > 0; i--) { + filesToSign.Add(pendingSign.Dequeue()); + } + + var filesToSignStr = String.Join(" ", filesToSign.Select(f => $"\"{f}\"")); + var command = $"/S /C \"\"{HelperFile.SignToolPath}\" sign {signArguments} {filesToSignStr} >> \"{signLogFile}\" 2>&1\""; + + var psi = new ProcessStartInfo { + FileName = "cmd.exe", + Arguments = command, + UseShellExecute = false, + WorkingDirectory = rootDir, + CreateNoWindow = true + }; + + var process = Process.Start(psi); + process.WaitForExit(); + + if (process.ExitCode != 0) { + var cmdWithPasswordHidden = "cmd.exe " + new Regex(@"\/p\s+?[^\s]+").Replace(command, "/p ********"); + Log.Debug($"Signing command failed: {cmdWithPasswordHidden}"); + var output = File.Exists(signLogFile) ? File.ReadAllText(signLogFile).Trim() : "No output file was created."; + throw new UserInfoException( + $"Signing command failed. Specify --verbose argument to print signing command.\n" + + $"Output was:" + Environment.NewLine + output); + } + + int processed = totalToSign - pendingSign.Count; + Log.Info($"Code-signed {processed}/{totalToSign} files"); + progress((int) ((double) processed / totalToSign * 100)); + } while (pendingSign.Count > 0); + + Log.Info("SignTool Output: " + Environment.NewLine + File.ReadAllText(signLogFile).Trim()); + } + + public void SignPEFileWithTemplate(string filePath, string signTemplate) + { + if (VelopackRuntimeInfo.IsWindows && CheckIsAlreadySigned(filePath)) { + Log.Debug($"'{filePath}' is already signed, and will not be signed again."); + return; + } + + var command = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\""); + + var result = Exe.InvokeProcess(command, null, null); + if (result.ExitCode != 0) { + var cmdWithPasswordHidden = new Regex(@"\/p\s+?[^\s]+").Replace(result.Command, "/p ********"); + Log.Debug($"Signing command failed: {cmdWithPasswordHidden}"); + throw new Exception( + $"Signing command failed. Specify --verbose argument to print signing command.\n\n" + + $"Output was:\n" + result.StdOutput); + } + + Log.Info("Sign successful: " + result.StdOutput); + } + + //private static ProcessStartInfo CreateProcessStartInfo(string fileName, string arguments, string workingDirectory = "") + //{ + // var psi = new ProcessStartInfo(fileName, arguments); + // psi.UseShellExecute = false; + // psi.WindowStyle = ProcessWindowStyle.Hidden; + // psi.ErrorDialog = false; + // psi.CreateNoWindow = true; + // psi.RedirectStandardOutput = true; + // psi.RedirectStandardError = true; + // psi.WorkingDirectory = workingDirectory; + // return psi; + //} + + //private void SignPEFile(string filePath, string signParams, string signTemplate) + //{ + // try { + // if (AuthenticodeTools.IsTrusted(filePath)) { + // Log.Debug($"'{filePath}' is already signed, skipping..."); + // return; + // } + // } catch (Exception ex) { + // Log.Error(ex, "Failed to determine signing status for " + filePath); + // } + + // string cmd; + // ProcessStartInfo psi; + // if (!String.IsNullOrEmpty(signParams)) { + // // use embedded signtool.exe with provided parameters + // cmd = $"sign {signParams} \"{filePath}\""; + // psi = CreateProcessStartInfo(HelperFile.SignToolPath, cmd); + // cmd = "signtool.exe " + cmd; + // } else if (!String.IsNullOrEmpty(signTemplate)) { + // // escape custom sign command and pass it to cmd.exe + // cmd = signTemplate.Replace("\"{{file}}\"", "{{file}}").Replace("{{file}}", $"\"{filePath}\""); + // psi = CreateProcessStartInfo("cmd", $"/c {EscapeCmdExeMetachars(cmd)}"); + // } else { + // Log.Debug($"{filePath} was not signed. (skipped; no signing parameters)"); + // return; + // } + + // var processResult = InvokeProcessUnsafeAsync(psi, CancellationToken.None) + // .ConfigureAwait(false).GetAwaiter().GetResult(); + + // if (processResult.ExitCode != 0) { + // var cmdWithPasswordHidden = new Regex(@"/p\s+\w+").Replace(cmd, "/p ********"); + // throw new Exception("Signing command failed: \n > " + cmdWithPasswordHidden + "\n" + processResult.StdOutput); + // } else { + // Log.Info("Sign successful: " + processResult.StdOutput); + // } + //} + + //private static string EscapeCmdExeMetachars(string command) + //{ + // var result = new StringBuilder(); + // foreach (var ch in command) { + // switch (ch) { + // case '(': + // case ')': + // case '%': + // case '!': + // case '^': + // case '"': + // case '<': + // case '>': + // case '&': + // case '|': + // result.Append('^'); + // break; + // } + // result.Append(ch); + // } + // return result.ToString(); + //} + + //private class ProcessResult + //{ + // public int ExitCode { get; set; } + // public string StdOutput { get; set; } + + // public ProcessResult(int exitCode, string stdOutput) + // { + // ExitCode = exitCode; + // StdOutput = stdOutput; + // } + //} + + //private static async Task InvokeProcessUnsafeAsync(ProcessStartInfo psi, CancellationToken ct) + //{ + // var pi = Process.Start(psi); + // await Task.Run(() => { + // while (!ct.IsCancellationRequested) { + // if (pi.WaitForExit(2000)) return; + // } + + // if (ct.IsCancellationRequested) { + // pi.Kill(); + // ct.ThrowIfCancellationRequested(); + // } + // }).ConfigureAwait(false); + + // string textResult = await pi.StandardOutput.ReadToEndAsync().ConfigureAwait(false); + // if (String.IsNullOrWhiteSpace(textResult) || pi.ExitCode != 0) { + // textResult = (textResult ?? "") + "\n" + await pi.StandardError.ReadToEndAsync().ConfigureAwait(false); + + // if (String.IsNullOrWhiteSpace(textResult)) { + // textResult = String.Empty; + // } + // } + + // return new ProcessResult(pi.ExitCode, textResult.Trim()); + //} } diff --git a/src/Velopack.Packaging.Windows/DotnetUtil.cs b/src/Velopack.Packaging.Windows/DotnetUtil.cs index 129e7f82..7f1d7a4d 100644 --- a/src/Velopack.Packaging.Windows/DotnetUtil.cs +++ b/src/Velopack.Packaging.Windows/DotnetUtil.cs @@ -8,145 +8,144 @@ using Microsoft.Extensions.Logging; using NuGet.Versioning; using Velopack.Packaging.Exceptions; -namespace Velopack.Packaging.Windows +namespace Velopack.Packaging.Windows; + +public class DotnetUtil { - public class DotnetUtil + public static NuGetVersion VerifyVelopackApp(string exeFile, ILogger log) { - public static NuGetVersion VerifyVelopackApp(string exeFile, ILogger log) - { - try { - NuGetVersion velopackVersion = null; - IPEImage velopackDll = null; - AssemblyDefinition mainAssy = null; - mainAssy ??= LoadFullFramework(exeFile, ref velopackDll); - mainAssy ??= LoadDncBundle(exeFile, ref velopackDll); + try { + NuGetVersion velopackVersion = null; + IPEImage velopackDll = null; + AssemblyDefinition mainAssy = null; + mainAssy ??= LoadFullFramework(exeFile, ref velopackDll); + mainAssy ??= LoadDncBundle(exeFile, ref velopackDll); - if (mainAssy == null) { - // not a dotnet binary - return null; + if (mainAssy == null) { + // not a dotnet binary + return null; + } + + if (velopackDll != null) { + try { + var versionInfo = VersionInfoResource.FromDirectory(velopackDll.Resources); + var actualInfo = versionInfo.GetChild(StringFileInfo.StringFileInfoKey); + var versionTable = actualInfo.Tables[0]; + var productVersion = versionTable.Where(v => v.Key == StringTable.ProductVersionKey).FirstOrDefault(); + velopackVersion = NuGetVersion.Parse(productVersion.Value); + } catch (Exception ex) { + // don't really care + log.Debug(ex, "Unable to read Velopack.dll version info."); } + } - if (velopackDll != null) { - try { - var versionInfo = VersionInfoResource.FromDirectory(velopackDll.Resources); - var actualInfo = versionInfo.GetChild(StringFileInfo.StringFileInfoKey); - var versionTable = actualInfo.Tables[0]; - var productVersion = versionTable.Where(v => v.Key == StringTable.ProductVersionKey).FirstOrDefault(); - velopackVersion = NuGetVersion.Parse(productVersion.Value); - } catch (Exception ex) { - // don't really care - log.Debug(ex, "Unable to read Velopack.dll version info."); - } - } + var mainModule = mainAssy.Modules.Single(); + var entryPoint = mainModule.ManagedEntryPointMethod; - var mainModule = mainAssy.Modules.Single(); - var entryPoint = mainModule.ManagedEntryPointMethod; - - foreach (var instr in entryPoint.CilMethodBody.Instructions) { - if (instr.OpCode.Code is CilCode.Call or CilCode.Callvirt or CilCode.Calli) { - SerializedMemberReference operand = instr.Operand as SerializedMemberReference; - if (operand != null && operand.IsMethod) { - if (operand.Name == "Run" && operand.DeclaringType.FullName == "Velopack.VelopackApp") { - // success! - if (velopackVersion != null) { - log.Info($"Verified VelopackApp.Run() in '{entryPoint.FullName}', version {velopackVersion}."); - if (velopackVersion != VelopackRuntimeInfo.VelopackProductVersion) { - log.Warn(exeFile + " was built with a different version of Velopack than this tool. " + - $"This may cause compatibility issues. Expected {VelopackRuntimeInfo.VelopackProductVersion}, " + - $"but found {velopackVersion}."); - } - return velopackVersion; - } else { - log.Warn("VelopackApp verified at entry point, but ProductVersion could not be checked."); - return null; + foreach (var instr in entryPoint.CilMethodBody.Instructions) { + if (instr.OpCode.Code is CilCode.Call or CilCode.Callvirt or CilCode.Calli) { + SerializedMemberReference operand = instr.Operand as SerializedMemberReference; + if (operand != null && operand.IsMethod) { + if (operand.Name == "Run" && operand.DeclaringType.FullName == "Velopack.VelopackApp") { + // success! + if (velopackVersion != null) { + log.Info($"Verified VelopackApp.Run() in '{entryPoint.FullName}', version {velopackVersion}."); + if (velopackVersion != VelopackRuntimeInfo.VelopackProductVersion) { + log.Warn(exeFile + " was built with a different version of Velopack than this tool. " + + $"This may cause compatibility issues. Expected {VelopackRuntimeInfo.VelopackProductVersion}, " + + $"but found {velopackVersion}."); } + return velopackVersion; + } else { + log.Warn("VelopackApp verified at entry point, but ProductVersion could not be checked."); + return null; } } } } - - // if we've iterated the whole main method and not found the call, then the velopack builder is missing - throw new UserInfoException($"Unable to verify VelopackApp, in application main method '{entryPoint.FullName}'. " + - "Please ensure that 'VelopackApp.Build().Run()' is present in your Program.Main()."); - - } catch (Exception ex) when (ex is not UserInfoException) { - log.Error("Unable to verify VelopackApp: " + ex.Message); } - return null; + // if we've iterated the whole main method and not found the call, then the velopack builder is missing + throw new UserInfoException($"Unable to verify VelopackApp, in application main method '{entryPoint.FullName}'. " + + "Please ensure that 'VelopackApp.Build().Run()' is present in your Program.Main()."); + + } catch (Exception ex) when (ex is not UserInfoException) { + log.Error("Unable to verify VelopackApp: " + ex.Message); } - private static AssemblyDefinition LoadFullFramework(string exeFile, ref IPEImage velopackDll) - { + return null; + } + + private static AssemblyDefinition LoadFullFramework(string exeFile, ref IPEImage velopackDll) + { + try { + var assy = AssemblyDefinition.FromFile(exeFile); + var versionFile = Path.Combine(Path.GetDirectoryName(exeFile), "Velopack.dll"); + if (File.Exists(versionFile)) { + velopackDll = PEImage.FromFile(versionFile); + } + return assy; + } catch (BadImageFormatException) { + // not a .Net Framework binary + return null; + } + } + + private static AssemblyDefinition LoadDncBundle(string exeFile, ref IPEImage velopackDll) + { + try { + var bundle = BundleManifest.FromFile(exeFile); + IList embeddedFiles = null; + try { - var assy = AssemblyDefinition.FromFile(exeFile); - var versionFile = Path.Combine(Path.GetDirectoryName(exeFile), "Velopack.dll"); + embeddedFiles = bundle.Files; + } catch { + // not a SingleFileHost binary, so we'll search on disk + var parentDir = Path.GetDirectoryName(exeFile); + var versionFile = Path.Combine(parentDir, "Velopack.dll"); if (File.Exists(versionFile)) { velopackDll = PEImage.FromFile(versionFile); } - return assy; - } catch (BadImageFormatException) { - // not a .Net Framework binary + + var diskFile = Path.Combine(parentDir, Path.GetFileNameWithoutExtension(exeFile) + ".dll"); + if (File.Exists(diskFile)) { + return AssemblyDefinition.FromFile(diskFile); + } + + var runtimeConfigFile = Directory.EnumerateFiles(parentDir, "*.runtimeconfig.json").SingleOrDefault(); + var possNameRuntime = Path.Combine(parentDir, + Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(runtimeConfigFile)) + ".dll"); + if (File.Exists(possNameRuntime)) { + return AssemblyDefinition.FromFile(possNameRuntime); + } + return null; } - } - private static AssemblyDefinition LoadDncBundle(string exeFile, ref IPEImage velopackDll) - { - try { - var bundle = BundleManifest.FromFile(exeFile); - IList embeddedFiles = null; - - try { - embeddedFiles = bundle.Files; - } catch { - // not a SingleFileHost binary, so we'll search on disk - var parentDir = Path.GetDirectoryName(exeFile); - var versionFile = Path.Combine(parentDir, "Velopack.dll"); - if (File.Exists(versionFile)) { - velopackDll = PEImage.FromFile(versionFile); - } - - var diskFile = Path.Combine(parentDir, Path.GetFileNameWithoutExtension(exeFile) + ".dll"); - if (File.Exists(diskFile)) { - return AssemblyDefinition.FromFile(diskFile); - } - - var runtimeConfigFile = Directory.EnumerateFiles(parentDir, "*.runtimeconfig.json").SingleOrDefault(); - var possNameRuntime = Path.Combine(parentDir, - Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(runtimeConfigFile)) + ".dll"); - if (File.Exists(possNameRuntime)) { - return AssemblyDefinition.FromFile(possNameRuntime); - } - - return null; - } - - var velopackEmbedded = embeddedFiles.SingleOrDefault(f => f.Type == BundleFileType.Assembly && f.RelativePath == "Velopack.dll"); - if (velopackEmbedded != null && velopackEmbedded.TryGetReader(out var readerVel)) { - velopackDll = PEImage.FromReader(readerVel); - } - - var runtimeConfig = embeddedFiles.SingleOrDefault(f => f.Type == BundleFileType.RuntimeConfigJson); - if (runtimeConfig != null) { - var possName1 = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(runtimeConfig.RelativePath)) + ".dll"; - var file = embeddedFiles.SingleOrDefault(f => f.Type == BundleFileType.Assembly && f.RelativePath == possName1); - if (file != null && file.TryGetReader(out var reader)) { - return AssemblyDefinition.FromReader(reader); - } - } - - var possName2 = Path.GetFileNameWithoutExtension(exeFile) + ".dll"; - var file2 = embeddedFiles.SingleOrDefault(f => f.Type == BundleFileType.Assembly && f.RelativePath == possName2); - if (file2 != null && file2.TryGetReader(out var reader2)) { - return AssemblyDefinition.FromReader(reader2); - } - - return null; - } catch (BadImageFormatException) { - // not an AppHost / SingleFileHost binary - return null; + var velopackEmbedded = embeddedFiles.SingleOrDefault(f => f.Type == BundleFileType.Assembly && f.RelativePath == "Velopack.dll"); + if (velopackEmbedded != null && velopackEmbedded.TryGetReader(out var readerVel)) { + velopackDll = PEImage.FromReader(readerVel); } + + var runtimeConfig = embeddedFiles.SingleOrDefault(f => f.Type == BundleFileType.RuntimeConfigJson); + if (runtimeConfig != null) { + var possName1 = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(runtimeConfig.RelativePath)) + ".dll"; + var file = embeddedFiles.SingleOrDefault(f => f.Type == BundleFileType.Assembly && f.RelativePath == possName1); + if (file != null && file.TryGetReader(out var reader)) { + return AssemblyDefinition.FromReader(reader); + } + } + + var possName2 = Path.GetFileNameWithoutExtension(exeFile) + ".dll"; + var file2 = embeddedFiles.SingleOrDefault(f => f.Type == BundleFileType.Assembly && f.RelativePath == possName2); + if (file2 != null && file2.TryGetReader(out var reader2)) { + return AssemblyDefinition.FromReader(reader2); + } + + return null; + } catch (BadImageFormatException) { + // not an AppHost / SingleFileHost binary + return null; } } } diff --git a/src/Velopack.Packaging.Windows/Rcedit.cs b/src/Velopack.Packaging.Windows/Rcedit.cs index 1d72b7b8..f07966da 100644 --- a/src/Velopack.Packaging.Windows/Rcedit.cs +++ b/src/Velopack.Packaging.Windows/Rcedit.cs @@ -6,38 +6,37 @@ using System.Text; using System.Threading.Tasks; using Velopack.NuGet; -namespace Velopack.Packaging.Windows +namespace Velopack.Packaging.Windows; + +[SupportedOSPlatform("windows")] +public class Rcedit { - [SupportedOSPlatform("windows")] - public class Rcedit + public static void SetExeIcon(string exePath, string iconPath) { - public static void SetExeIcon(string exePath, string iconPath) - { - var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath }; - Utility.Retry(() => Exe.InvokeAndThrowIfNonZero(HelperFile.RceditPath, args, null)); + var args = new[] { Path.GetFullPath(exePath), "--set-icon", iconPath }; + Utility.Retry(() => Exe.InvokeAndThrowIfNonZero(HelperFile.RceditPath, args, null)); + } + + [SupportedOSPlatform("windows")] + public static void SetPEVersionBlockFromPackageInfo(string exePath, PackageManifest package, string iconPath = null) + { + var realExePath = Path.GetFullPath(exePath); + + List args = new List() { + realExePath, + "--set-version-string", "CompanyName", package.ProductCompany, + "--set-version-string", "LegalCopyright", package.ProductCopyright, + "--set-version-string", "FileDescription", package.ProductDescription, + "--set-version-string", "ProductName", package.ProductName, + "--set-file-version", package.Version.ToString(), + "--set-product-version", package.Version.ToString(), + }; + + if (iconPath != null) { + args.Add("--set-icon"); + args.Add(Path.GetFullPath(iconPath)); } - [SupportedOSPlatform("windows")] - public static void SetPEVersionBlockFromPackageInfo(string exePath, PackageManifest package, string iconPath = null) - { - var realExePath = Path.GetFullPath(exePath); - - List args = new List() { - realExePath, - "--set-version-string", "CompanyName", package.ProductCompany, - "--set-version-string", "LegalCopyright", package.ProductCopyright, - "--set-version-string", "FileDescription", package.ProductDescription, - "--set-version-string", "ProductName", package.ProductName, - "--set-file-version", package.Version.ToString(), - "--set-product-version", package.Version.ToString(), - }; - - if (iconPath != null) { - args.Add("--set-icon"); - args.Add(Path.GetFullPath(iconPath)); - } - - Utility.Retry(() => Exe.InvokeAndThrowIfNonZero(HelperFile.RceditPath, args, null)); - } + Utility.Retry(() => Exe.InvokeAndThrowIfNonZero(HelperFile.RceditPath, args, null)); } } diff --git a/src/Velopack.Packaging/Abstractions/ICommand.cs b/src/Velopack.Packaging/Abstractions/ICommand.cs index 9d98799b..d498c7b3 100644 --- a/src/Velopack.Packaging/Abstractions/ICommand.cs +++ b/src/Velopack.Packaging/Abstractions/ICommand.cs @@ -1,9 +1,8 @@ using Microsoft.Extensions.Logging; -namespace Velopack.Packaging.Abstractions +namespace Velopack.Packaging.Abstractions; + +public interface ICommand where TOpt : class { - public interface ICommand where TOpt : class - { - Task Run(TOpt options); - } + Task Run(TOpt options); } diff --git a/src/Velopack.Packaging/Abstractions/IFancyConsole.cs b/src/Velopack.Packaging/Abstractions/IFancyConsole.cs index 3fad5171..7881201b 100644 --- a/src/Velopack.Packaging/Abstractions/IFancyConsole.cs +++ b/src/Velopack.Packaging/Abstractions/IFancyConsole.cs @@ -1,15 +1,14 @@ using Microsoft.Extensions.Logging; -namespace Velopack.Packaging.Abstractions +namespace Velopack.Packaging.Abstractions; + +public interface IFancyConsole { - public interface IFancyConsole - { - Task ExecuteProgressAsync(Func action); + Task ExecuteProgressAsync(Func action); - void WriteTable(string tableName, IEnumerable> rows, bool hasHeaderRow = true); + void WriteTable(string tableName, IEnumerable> rows, bool hasHeaderRow = true); - Task PromptYesNo(string prompt, bool? defaultValue = null, TimeSpan? timeout = null); + Task PromptYesNo(string prompt, bool? defaultValue = null, TimeSpan? timeout = null); - void WriteLine(string text = ""); - } + void WriteLine(string text = ""); } diff --git a/src/Velopack.Packaging/Abstractions/IFancyConsoleProgress.cs b/src/Velopack.Packaging/Abstractions/IFancyConsoleProgress.cs index 11d0c777..cf89767b 100644 --- a/src/Velopack.Packaging/Abstractions/IFancyConsoleProgress.cs +++ b/src/Velopack.Packaging/Abstractions/IFancyConsoleProgress.cs @@ -1,7 +1,6 @@ -namespace Velopack.Packaging.Abstractions +namespace Velopack.Packaging.Abstractions; + +public interface IFancyConsoleProgress { - public interface IFancyConsoleProgress - { - Task RunTask(string name, Func, Task> fn); - } + Task RunTask(string name, Func, Task> fn); } diff --git a/src/Velopack.Packaging/Abstractions/IOutputOptions.cs b/src/Velopack.Packaging/Abstractions/IOutputOptions.cs index a9698e7e..16e64995 100644 --- a/src/Velopack.Packaging/Abstractions/IOutputOptions.cs +++ b/src/Velopack.Packaging/Abstractions/IOutputOptions.cs @@ -4,10 +4,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Velopack.Packaging.Abstractions +namespace Velopack.Packaging.Abstractions; + +public interface IOutputOptions { - public interface IOutputOptions - { - DirectoryInfo ReleaseDir { get; } - } + DirectoryInfo ReleaseDir { get; } } diff --git a/src/Velopack.Packaging/Abstractions/IPackOptions.cs b/src/Velopack.Packaging/Abstractions/IPackOptions.cs index 67771cfe..24d88f4b 100644 --- a/src/Velopack.Packaging/Abstractions/IPackOptions.cs +++ b/src/Velopack.Packaging/Abstractions/IPackOptions.cs @@ -1,9 +1,8 @@ -namespace Velopack.Packaging.Abstractions +namespace Velopack.Packaging.Abstractions; + +public interface IPackOptions : INugetPackCommand, IPlatformOptions { - public interface IPackOptions : INugetPackCommand, IPlatformOptions - { - string Channel { get; } - DeltaMode DeltaMode { get; } - string EntryExecutableName { get; } - } + string Channel { get; } + DeltaMode DeltaMode { get; } + string EntryExecutableName { get; } } diff --git a/src/Velopack.Packaging/Abstractions/IPlatformOptions.cs b/src/Velopack.Packaging/Abstractions/IPlatformOptions.cs index 02650da2..763359a2 100644 --- a/src/Velopack.Packaging/Abstractions/IPlatformOptions.cs +++ b/src/Velopack.Packaging/Abstractions/IPlatformOptions.cs @@ -4,10 +4,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Velopack.Packaging.Abstractions +namespace Velopack.Packaging.Abstractions; + +public interface IPlatformOptions : IOutputOptions { - public interface IPlatformOptions : IOutputOptions - { - RID TargetRuntime { get; } - } + RID TargetRuntime { get; } } diff --git a/src/Velopack.Packaging/BuildAssets.cs b/src/Velopack.Packaging/BuildAssets.cs index 59ba5b4e..0bb8470b 100644 --- a/src/Velopack.Packaging/BuildAssets.cs +++ b/src/Velopack.Packaging/BuildAssets.cs @@ -1,37 +1,36 @@ using Velopack.Json; using Velopack.Packaging.Exceptions; -namespace Velopack.Packaging +namespace Velopack.Packaging; + +public class BuildAssets { - public class BuildAssets + public List Files { get; set; } = new List(); + + public List GetReleaseEntries() { - public List Files { get; set; } = new List(); + return Files.Where(x => x.EndsWith(".nupkg")) + .Select(f => VelopackAsset.FromNupkg(f)) + .ToList(); + } - public List GetReleaseEntries() - { - return Files.Where(x => x.EndsWith(".nupkg")) - .Select(f => VelopackAsset.FromNupkg(f)) - .ToList(); - } + public static void Write(string outputDir, string channel, IEnumerable files) + { + var assets = new BuildAssets { + Files = files.OrderBy(f => f).ToList(), + }; + var path = Path.Combine(outputDir, $"assets.{channel}.json"); + var json = SimpleJson.SerializeObject(assets); + File.WriteAllText(path, json); + } - public static void Write(string outputDir, string channel, IEnumerable files) - { - var assets = new BuildAssets { - Files = files.OrderBy(f => f).ToList(), - }; - var path = Path.Combine(outputDir, $"assets.{channel}.json"); - var json = SimpleJson.SerializeObject(assets); - File.WriteAllText(path, json); - } - - public static BuildAssets Read(string outputDir, string channel) - { - var path = Path.Combine(outputDir, $"assets.{channel}.json"); - if (!File.Exists(path)) { - throw new UserInfoException($"Could not find assets file for channel '{channel}' (looking for '{Path.GetFileName(path)}' in directory '{outputDir}'). " + - $"If you've just created a Velopack release, verify you're calling this command with the same '--channel' as you did with 'pack'."); - } - return SimpleJson.DeserializeObject(File.ReadAllText(path)); + public static BuildAssets Read(string outputDir, string channel) + { + var path = Path.Combine(outputDir, $"assets.{channel}.json"); + if (!File.Exists(path)) { + throw new UserInfoException($"Could not find assets file for channel '{channel}' (looking for '{Path.GetFileName(path)}' in directory '{outputDir}'). " + + $"If you've just created a Velopack release, verify you're calling this command with the same '--channel' as you did with 'pack'."); } + return SimpleJson.DeserializeObject(File.ReadAllText(path)); } } diff --git a/src/Velopack.Packaging/Commands/DeltaGenCommandRunner.cs b/src/Velopack.Packaging/Commands/DeltaGenCommandRunner.cs index 023b2606..3d3ae6ba 100644 --- a/src/Velopack.Packaging/Commands/DeltaGenCommandRunner.cs +++ b/src/Velopack.Packaging/Commands/DeltaGenCommandRunner.cs @@ -1,31 +1,30 @@ using Microsoft.Extensions.Logging; using Velopack.Packaging.Abstractions; -namespace Velopack.Packaging.Commands +namespace Velopack.Packaging.Commands; + +public class DeltaGenCommandRunner : ICommand { - public class DeltaGenCommandRunner : ICommand + private readonly ILogger _logger; + private readonly IFancyConsole _console; + + public DeltaGenCommandRunner(ILogger logger, IFancyConsole console) { - private readonly ILogger _logger; - private readonly IFancyConsole _console; + _logger = logger; + _console = console; + } - public DeltaGenCommandRunner(ILogger logger, IFancyConsole console) - { - _logger = logger; - _console = console; - } - - public async Task Run(DeltaGenOptions options) - { - await _console.ExecuteProgressAsync(async (ctx) => { - var pold = new ReleasePackage(options.BasePackage); - var pnew = new ReleasePackage(options.NewPackage); - await ctx.RunTask($"Building delta {pold.Version} -> {pnew.Version}", (progress) => { - var delta = new DeltaPackageBuilder(_logger); - delta.CreateDeltaPackage(pold, pnew, options.OutputFile, options.DeltaMode, progress); - progress(100); - return Task.CompletedTask; - }); + public async Task Run(DeltaGenOptions options) + { + await _console.ExecuteProgressAsync(async (ctx) => { + var pold = new ReleasePackage(options.BasePackage); + var pnew = new ReleasePackage(options.NewPackage); + await ctx.RunTask($"Building delta {pold.Version} -> {pnew.Version}", (progress) => { + var delta = new DeltaPackageBuilder(_logger); + delta.CreateDeltaPackage(pold, pnew, options.OutputFile, options.DeltaMode, progress); + progress(100); + return Task.CompletedTask; }); - } + }); } } diff --git a/src/Velopack.Packaging/Commands/DeltaGenOptions.cs b/src/Velopack.Packaging/Commands/DeltaGenOptions.cs index 2daa3ba3..8de0f415 100644 --- a/src/Velopack.Packaging/Commands/DeltaGenOptions.cs +++ b/src/Velopack.Packaging/Commands/DeltaGenOptions.cs @@ -1,13 +1,12 @@ -namespace Velopack.Packaging.Commands +namespace Velopack.Packaging.Commands; + +public class DeltaGenOptions { - public class DeltaGenOptions - { - public DeltaMode DeltaMode { get; set; } + public DeltaMode DeltaMode { get; set; } - public string BasePackage { get; set; } + public string BasePackage { get; set; } - public string NewPackage { get; set; } + public string NewPackage { get; set; } - public string OutputFile { get; set; } - } + public string OutputFile { get; set; } } diff --git a/src/Velopack.Packaging/Commands/DeltaPatchCommandRunner.cs b/src/Velopack.Packaging/Commands/DeltaPatchCommandRunner.cs index af135f59..5dc98037 100644 --- a/src/Velopack.Packaging/Commands/DeltaPatchCommandRunner.cs +++ b/src/Velopack.Packaging/Commands/DeltaPatchCommandRunner.cs @@ -3,50 +3,49 @@ using Velopack.Compression; using Velopack.Packaging.Exceptions; using Velopack.Packaging.Abstractions; -namespace Velopack.Packaging.Commands +namespace Velopack.Packaging.Commands; + +public class DeltaPatchCommandRunner : ICommand { - public class DeltaPatchCommandRunner : ICommand + private readonly ILogger _logger; + private readonly IFancyConsole _console; + + public DeltaPatchCommandRunner(ILogger logger, IFancyConsole console) { - private readonly ILogger _logger; - private readonly IFancyConsole _console; + _logger = logger; + _console = console; + } - public DeltaPatchCommandRunner(ILogger logger, IFancyConsole console) - { - _logger = logger; - _console = console; + public async Task Run(DeltaPatchOptions options) + { + if (options.PatchFiles.Length == 0) { + throw new UserInfoException("Must specify at least one patch file."); } - public async Task Run(DeltaPatchOptions options) - { - if (options.PatchFiles.Length == 0) { - throw new UserInfoException("Must specify at least one patch file."); + foreach (var p in options.PatchFiles) { + if (p == null || !p.Exists) { + throw new UserInfoException($"Patch file '{p.FullName}' does not exist."); } + } - foreach (var p in options.PatchFiles) { - if (p == null || !p.Exists) { - throw new UserInfoException($"Patch file '{p.FullName}' does not exist."); - } - } + var tmp = Utility.GetDefaultTempBaseDirectory(); + using var _1 = Utility.GetTempDirectory(out var workDir); - var tmp = Utility.GetDefaultTempBaseDirectory(); - using var _1 = Utility.GetTempDirectory(out var workDir); + var delta = new DeltaEmbedded(HelperFile.GetZstdPath(), _logger, tmp); + EasyZip.ExtractZipToDirectory(_logger, options.BasePackage, workDir); - var delta = new DeltaEmbedded(HelperFile.GetZstdPath(), _logger, tmp); - EasyZip.ExtractZipToDirectory(_logger, options.BasePackage, workDir); - - await _console.ExecuteProgressAsync(async (ctx) => { - foreach (var f in options.PatchFiles) { - await ctx.RunTask($"Applying {f.Name}", (progress) => { - delta.ApplyDeltaPackageFast(workDir, f.FullName, progress); - progress(100); - return Task.CompletedTask; - }); - } - await ctx.RunTask($"Building {Path.GetFileName(options.OutputFile)}", async (progress) => { - await EasyZip.CreateZipFromDirectoryAsync(_logger, options.OutputFile, workDir, progress); + await _console.ExecuteProgressAsync(async (ctx) => { + foreach (var f in options.PatchFiles) { + await ctx.RunTask($"Applying {f.Name}", (progress) => { + delta.ApplyDeltaPackageFast(workDir, f.FullName, progress); progress(100); + return Task.CompletedTask; }); + } + await ctx.RunTask($"Building {Path.GetFileName(options.OutputFile)}", async (progress) => { + await EasyZip.CreateZipFromDirectoryAsync(_logger, options.OutputFile, workDir, progress); + progress(100); }); - } + }); } } diff --git a/src/Velopack.Packaging/Commands/DeltaPatchOptions.cs b/src/Velopack.Packaging/Commands/DeltaPatchOptions.cs index df604cb3..f742f892 100644 --- a/src/Velopack.Packaging/Commands/DeltaPatchOptions.cs +++ b/src/Velopack.Packaging/Commands/DeltaPatchOptions.cs @@ -1,11 +1,10 @@ -namespace Velopack.Packaging.Commands +namespace Velopack.Packaging.Commands; + +public class DeltaPatchOptions { - public class DeltaPatchOptions - { - public string BasePackage { get; set; } + public string BasePackage { get; set; } - public FileInfo[] PatchFiles { get; set; } + public FileInfo[] PatchFiles { get; set; } - public string OutputFile { get; set; } - } + public string OutputFile { get; set; } } diff --git a/src/Velopack.Packaging/DeltaEmbedded.cs b/src/Velopack.Packaging/DeltaEmbedded.cs index 69230d94..10560438 100644 --- a/src/Velopack.Packaging/DeltaEmbedded.cs +++ b/src/Velopack.Packaging/DeltaEmbedded.cs @@ -1,35 +1,34 @@ using Microsoft.Extensions.Logging; using Velopack.Compression; -namespace Velopack.Packaging +namespace Velopack.Packaging; + +public class DeltaEmbedded { - public class DeltaEmbedded + private readonly DeltaImpl _delta; + + public DeltaEmbedded(string zstdPath, ILogger logger, string baseTmpDir) { - private readonly DeltaImpl _delta; + _delta = new DeltaImpl(zstdPath, logger, baseTmpDir); + } - public DeltaEmbedded(string zstdPath, ILogger logger, string baseTmpDir) + public void ApplyDeltaPackageFast(string workingPath, string deltaPackageZip, Action progress = null) + { + _delta.ApplyDeltaPackageFast(workingPath, deltaPackageZip, progress); + } + + private class DeltaImpl : DeltaPackage + { + private readonly Zstd _zstd; + + public DeltaImpl(string zstdPath, ILogger logger, string baseTmpDir) : base(logger, baseTmpDir) { - _delta = new DeltaImpl(zstdPath, logger, baseTmpDir); + _zstd = new Zstd(zstdPath); } - public void ApplyDeltaPackageFast(string workingPath, string deltaPackageZip, Action progress = null) + protected override void ApplyZstdPatch(string baseFile, string patchFile, string outputFile) { - _delta.ApplyDeltaPackageFast(workingPath, deltaPackageZip, progress); - } - - private class DeltaImpl : DeltaPackage - { - private readonly Zstd _zstd; - - public DeltaImpl(string zstdPath, ILogger logger, string baseTmpDir) : base(logger, baseTmpDir) - { - _zstd = new Zstd(zstdPath); - } - - protected override void ApplyZstdPatch(string baseFile, string patchFile, string outputFile) - { - _zstd.ApplyPatch(baseFile, patchFile, outputFile); - } + _zstd.ApplyPatch(baseFile, patchFile, outputFile); } } } \ No newline at end of file diff --git a/src/Velopack.Packaging/Exceptions/UserInfoException.cs b/src/Velopack.Packaging/Exceptions/UserInfoException.cs index bef133db..6c607b05 100644 --- a/src/Velopack.Packaging/Exceptions/UserInfoException.cs +++ b/src/Velopack.Packaging/Exceptions/UserInfoException.cs @@ -6,28 +6,27 @@ using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; -namespace Velopack.Packaging.Exceptions +namespace Velopack.Packaging.Exceptions; + +/// +/// Denotes that an error has occurred for which a stack trace should not be printed. +/// +[ExcludeFromCodeCoverage] +public class UserInfoException : Exception { - /// - /// Denotes that an error has occurred for which a stack trace should not be printed. - /// - [ExcludeFromCodeCoverage] - public class UserInfoException : Exception + public UserInfoException() { - public UserInfoException() - { - } + } - public UserInfoException(string message) : base(message) - { - } + public UserInfoException(string message) : base(message) + { + } - public UserInfoException(string message, Exception innerException) : base(message, innerException) - { - } + public UserInfoException(string message, Exception innerException) : base(message, innerException) + { + } - protected UserInfoException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } + protected UserInfoException(SerializationInfo info, StreamingContext context) : base(info, context) + { } } diff --git a/src/Velopack.Packaging/Exceptions/VelopackAppVerificationException.cs b/src/Velopack.Packaging/Exceptions/VelopackAppVerificationException.cs index b10bf793..60c7d653 100644 --- a/src/Velopack.Packaging/Exceptions/VelopackAppVerificationException.cs +++ b/src/Velopack.Packaging/Exceptions/VelopackAppVerificationException.cs @@ -5,17 +5,16 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Velopack.Packaging.Exceptions +namespace Velopack.Packaging.Exceptions; + +[ExcludeFromCodeCoverage] +public class VelopackAppVerificationException : UserInfoException { - [ExcludeFromCodeCoverage] - public class VelopackAppVerificationException : UserInfoException + public VelopackAppVerificationException(string message) + : base( + $"Failed to verify VelopackApp ({message}). " + + $"Ensure you have added the startup code to the beginning of your Program.Main(): VelopackApp.Build().Run(); " + + $"and then re-compile/re-publish your application.") { - public VelopackAppVerificationException(string message) - : base( - $"Failed to verify VelopackApp ({message}). " + - $"Ensure you have added the startup code to the beginning of your Program.Main(): VelopackApp.Build().Run(); " + - $"and then re-compile/re-publish your application.") - { - } } } diff --git a/src/Velopack.Packaging/Exe.cs b/src/Velopack.Packaging/Exe.cs index b489364c..2a18868d 100644 --- a/src/Velopack.Packaging/Exe.cs +++ b/src/Velopack.Packaging/Exe.cs @@ -6,79 +6,78 @@ using System.Text; using System.Threading.Tasks; using Velopack.Packaging.Exceptions; -namespace Velopack.Packaging +namespace Velopack.Packaging; + +public static class Exe { - public static class Exe + public static void AssertSystemBinaryExists(string binaryName) { - public static void AssertSystemBinaryExists(string binaryName) - { - try { - if (VelopackRuntimeInfo.IsWindows) { - var output = InvokeAndThrowIfNonZero("where", new[] { binaryName }, null); - if (String.IsNullOrWhiteSpace(output) || !File.Exists(output)) - throw new ProcessFailedException("", ""); - } else if (VelopackRuntimeInfo.IsOSX) { - InvokeAndThrowIfNonZero("command", new[] { "-v", binaryName }, null); - } else if (VelopackRuntimeInfo.IsLinux) { - InvokeAndThrowIfNonZero("which", new[] { binaryName }, null); - } else { - throw new PlatformNotSupportedException(); - } - } catch (ProcessFailedException) { - throw new Exception($"Could not find '{binaryName}' on the system, ensure it is installed and on the PATH."); + try { + if (VelopackRuntimeInfo.IsWindows) { + var output = InvokeAndThrowIfNonZero("where", new[] { binaryName }, null); + if (String.IsNullOrWhiteSpace(output) || !File.Exists(output)) + throw new ProcessFailedException("", ""); + } else if (VelopackRuntimeInfo.IsOSX) { + InvokeAndThrowIfNonZero("command", new[] { "-v", binaryName }, null); + } else if (VelopackRuntimeInfo.IsLinux) { + InvokeAndThrowIfNonZero("which", new[] { binaryName }, null); + } else { + throw new PlatformNotSupportedException(); } - } - - public static string InvokeAndThrowIfNonZero(string exePath, IEnumerable args, string workingDir, IDictionary envVar = null) - { - var result = InvokeProcess(exePath, args, workingDir, CancellationToken.None, envVar); - ProcessFailedException.ThrowIfNonZero(result); - return result.StdOutput; - } - - public static (int ExitCode, string StdOutput) InvokeProcess(ProcessStartInfo psi, CancellationToken ct) - { - var pi = Process.Start(psi); - while (!ct.IsCancellationRequested) { - if (pi.WaitForExit(500)) break; - } - - if (ct.IsCancellationRequested && !pi.HasExited) { - pi.Kill(); - ct.ThrowIfCancellationRequested(); - } - - string output = pi.StandardOutput.ReadToEnd(); - string error = pi.StandardError.ReadToEnd(); - var all = (output ?? "") + Environment.NewLine + (error ?? ""); - - return (pi.ExitCode, all.Trim()); - } - - public static (int ExitCode, string StdOutput, string Command) InvokeProcess(string fileName, IEnumerable args, string workingDirectory, CancellationToken ct = default, IDictionary envVar = null) - { - var psi = CreateProcessStartInfo(fileName, workingDirectory); - if (envVar != null) { - foreach (var kvp in envVar) { - psi.EnvironmentVariables[kvp.Key] = kvp.Value; - } - } - psi.AppendArgumentListSafe(args, out var argString); - var p = InvokeProcess(psi, ct); - return (p.ExitCode, p.StdOutput, $"{fileName} {argString}"); - } - - public static ProcessStartInfo CreateProcessStartInfo(string fileName, string workingDirectory) - { - var psi = new ProcessStartInfo(fileName); - psi.UseShellExecute = false; - psi.WindowStyle = ProcessWindowStyle.Hidden; - psi.ErrorDialog = false; - psi.CreateNoWindow = true; - psi.RedirectStandardOutput = true; - psi.RedirectStandardError = true; - psi.WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory; - return psi; + } catch (ProcessFailedException) { + throw new Exception($"Could not find '{binaryName}' on the system, ensure it is installed and on the PATH."); } } + + public static string InvokeAndThrowIfNonZero(string exePath, IEnumerable args, string workingDir, IDictionary envVar = null) + { + var result = InvokeProcess(exePath, args, workingDir, CancellationToken.None, envVar); + ProcessFailedException.ThrowIfNonZero(result); + return result.StdOutput; + } + + public static (int ExitCode, string StdOutput) InvokeProcess(ProcessStartInfo psi, CancellationToken ct) + { + var pi = Process.Start(psi); + while (!ct.IsCancellationRequested) { + if (pi.WaitForExit(500)) break; + } + + if (ct.IsCancellationRequested && !pi.HasExited) { + pi.Kill(); + ct.ThrowIfCancellationRequested(); + } + + string output = pi.StandardOutput.ReadToEnd(); + string error = pi.StandardError.ReadToEnd(); + var all = (output ?? "") + Environment.NewLine + (error ?? ""); + + return (pi.ExitCode, all.Trim()); + } + + public static (int ExitCode, string StdOutput, string Command) InvokeProcess(string fileName, IEnumerable args, string workingDirectory, CancellationToken ct = default, IDictionary envVar = null) + { + var psi = CreateProcessStartInfo(fileName, workingDirectory); + if (envVar != null) { + foreach (var kvp in envVar) { + psi.EnvironmentVariables[kvp.Key] = kvp.Value; + } + } + psi.AppendArgumentListSafe(args, out var argString); + var p = InvokeProcess(psi, ct); + return (p.ExitCode, p.StdOutput, $"{fileName} {argString}"); + } + + public static ProcessStartInfo CreateProcessStartInfo(string fileName, string workingDirectory) + { + var psi = new ProcessStartInfo(fileName); + psi.UseShellExecute = false; + psi.WindowStyle = ProcessWindowStyle.Hidden; + psi.ErrorDialog = false; + psi.CreateNoWindow = true; + psi.RedirectStandardOutput = true; + psi.RedirectStandardError = true; + psi.WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory; + return psi; + } } diff --git a/src/Velopack.Packaging/PackageBuilder.cs b/src/Velopack.Packaging/PackageBuilder.cs index 01f1722f..d07cc95c 100644 --- a/src/Velopack.Packaging/PackageBuilder.cs +++ b/src/Velopack.Packaging/PackageBuilder.cs @@ -9,204 +9,204 @@ using Velopack.NuGet; using Velopack.Packaging.Abstractions; using Velopack.Packaging.Exceptions; -namespace Velopack.Packaging +namespace Velopack.Packaging; + +public abstract class PackageBuilder : ICommand + where T : class, IPackOptions { - public abstract class PackageBuilder : ICommand - where T : class, IPackOptions + protected RuntimeOs SupportedTargetOs { get; } + + protected ILogger Log { get; } + + protected IFancyConsole Console { get; } + + protected DirectoryInfo TempDir { get; private set; } + + protected T Options { get; private set; } + + protected string MainExeName { get; private set; } + + protected string MainExePath { get; private set; } + + protected string Channel { get; private set; } + + protected string RuntimeDependencies { get; private set; } + + private readonly Regex REGEX_EXCLUDES = new Regex(@".*[\\\/]createdump.*|.*\.vshost\..*|.*\.nupkg$|.*\.pdb$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + public PackageBuilder(RuntimeOs supportedOs, ILogger logger, IFancyConsole console) { - protected RuntimeOs SupportedTargetOs { get; } + SupportedTargetOs = supportedOs; + Log = logger; + Console = console; + } - protected ILogger Log { get; } + public async Task Run(T options) + { + if (options.TargetRuntime?.BaseRID != SupportedTargetOs) + throw new UserInfoException($"To build packages for {SupportedTargetOs.GetOsLongName()}, " + + $"the target rid must be {SupportedTargetOs} (actually was {options.TargetRuntime?.BaseRID})."); - protected IFancyConsole Console { get; } + Log.Info("Beginning to package release."); + Log.Info("Releases Directory: " + options.ReleaseDir.FullName); - protected DirectoryInfo TempDir { get; private set; } + var releaseDir = options.ReleaseDir; + var channel = options.Channel?.ToLower() ?? ReleaseEntryHelper.GetDefaultChannel(SupportedTargetOs); + Channel = channel; - protected T Options { get; private set; } - - protected string MainExeName { get; private set; } - - protected string MainExePath { get; private set; } - - protected string Channel { get; private set; } - - protected string RuntimeDependencies { get; private set; } - - private readonly Regex REGEX_EXCLUDES = new Regex(@".*[\\\/]createdump.*|.*\.vshost\..*|.*\.nupkg$|.*\.pdb$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - public PackageBuilder(RuntimeOs supportedOs, ILogger logger, IFancyConsole console) - { - SupportedTargetOs = supportedOs; - Log = logger; - Console = console; + var entryHelper = new ReleaseEntryHelper(releaseDir.FullName, channel, Log); + if (entryHelper.DoesSimilarVersionExist(SemanticVersion.Parse(options.PackVersion))) { + if (await Console.PromptYesNo("A release in this channel with the same or greater version already exists. Do you want to continue and potentially overwrite files?") != true) { + throw new UserInfoException($"There is a release in channel {channel} which is equal or greater to the current version {options.PackVersion}. Please increase the current package version or remove that release."); + } } - public async Task Run(T options) + var packId = options.PackId; + var packDirectory = options.PackDirectory; + var packVersion = options.PackVersion; + var semVer = SemanticVersion.Parse(packVersion); + + // check that entry exe exists + var mainExt = options.TargetRuntime.BaseRID == RuntimeOs.Windows ? ".exe" : ""; + var mainExeName = options.EntryExecutableName ?? (options.PackId + mainExt); + var mainExePath = Path.Combine(packDirectory, mainExeName); + + // TODO: this is a hack, fix this. + if (!File.Exists(mainExePath) && VelopackRuntimeInfo.IsLinux) + mainExePath = Path.Combine(packDirectory, "usr", "bin", mainExeName); + + if (!File.Exists(mainExePath)) { + throw new UserInfoException( + $"Could not find main application executable (the one that runs 'VelopackApp.Build().Run()'). " + Environment.NewLine + + $"I searched for '{mainExeName}' in {packDirectory}." + Environment.NewLine + + $"If your main binary is not named '{mainExeName}', please specify the name with the argument: --exeName {{yourBinary.exe}}"); + } + MainExeName = mainExeName; + MainExePath = mainExePath; + + using var _1 = Utility.GetTempDirectory(out var pkgTempDir); + TempDir = new DirectoryInfo(pkgTempDir); + Options = options; + RuntimeDependencies = GetRuntimeDependencies(); + + ConcurrentBag<(string from, string to)> filesToCopy = new(); + + string getIncompletePath(string fileName) { - if (options.TargetRuntime?.BaseRID != SupportedTargetOs) - throw new UserInfoException($"To build packages for {SupportedTargetOs.GetOsLongName()}, " + - $"the target rid must be {SupportedTargetOs} (actually was {options.TargetRuntime?.BaseRID})."); + var incomplete = Path.Combine(pkgTempDir, fileName); + var final = Path.Combine(releaseDir.FullName, fileName); + try { File.Delete(incomplete); } catch { } + filesToCopy.Add((incomplete, final)); + return incomplete; + } - Log.Info("Beginning to package release."); - Log.Info("Releases Directory: " + options.ReleaseDir.FullName); - - var releaseDir = options.ReleaseDir; - var channel = options.Channel?.ToLower() ?? ReleaseEntryHelper.GetDefaultChannel(SupportedTargetOs); - Channel = channel; - - var entryHelper = new ReleaseEntryHelper(releaseDir.FullName, channel, Log); - if (entryHelper.DoesSimilarVersionExist(SemanticVersion.Parse(options.PackVersion))) { - if (await Console.PromptYesNo("A release in this channel with the same or greater version already exists. Do you want to continue and potentially overwrite files?") != true) { - throw new UserInfoException($"There is a release in channel {channel} which is equal or greater to the current version {options.PackVersion}. Please increase the current package version or remove that release."); - } - } - - var packId = options.PackId; - var packDirectory = options.PackDirectory; - var packVersion = options.PackVersion; - var semVer = SemanticVersion.Parse(packVersion); - - // check that entry exe exists - var mainExt = options.TargetRuntime.BaseRID == RuntimeOs.Windows ? ".exe" : ""; - var mainExeName = options.EntryExecutableName ?? (options.PackId + mainExt); - var mainExePath = Path.Combine(packDirectory, mainExeName); - - // TODO: this is a hack, fix this. - if (!File.Exists(mainExePath) && VelopackRuntimeInfo.IsLinux) - mainExePath = Path.Combine(packDirectory, "usr", "bin", mainExeName); - - if (!File.Exists(mainExePath)) { - throw new UserInfoException( - $"Could not find main application executable (the one that runs 'VelopackApp.Build().Run()'). " + Environment.NewLine + - $"I searched for '{mainExeName}' in {packDirectory}." + Environment.NewLine + - $"If your main binary is not named '{mainExeName}', please specify the name with the argument: --exeName {{yourBinary.exe}}"); - } - MainExeName = mainExeName; - MainExePath = mainExePath; - - using var _1 = Utility.GetTempDirectory(out var pkgTempDir); - TempDir = new DirectoryInfo(pkgTempDir); - Options = options; - RuntimeDependencies = GetRuntimeDependencies(); - - ConcurrentBag<(string from, string to)> filesToCopy = new(); - - string getIncompletePath(string fileName) - { - var incomplete = Path.Combine(pkgTempDir, fileName); - var final = Path.Combine(releaseDir.FullName, fileName); - try { File.Delete(incomplete); } catch { } - filesToCopy.Add((incomplete, final)); - return incomplete; - } - - await Console.ExecuteProgressAsync(async (ctx) => { - ReleasePackage prev = null; - await ctx.RunTask("Pre-process steps", async (progress) => { - prev = entryHelper.GetPreviousFullRelease(NuGetVersion.Parse(packVersion)); - packDirectory = await PreprocessPackDir(progress, packDirectory); - }); - - if (VelopackRuntimeInfo.IsWindows || VelopackRuntimeInfo.IsOSX) { - await ctx.RunTask("Code-sign application", async (progress) => { - await CodeSign(progress, packDirectory); - }); - } - - var portableTask = ctx.RunTask("Building portable package", async (progress) => { - var suggestedName = ReleaseEntryHelper.GetSuggestedPortableName(packId, channel); - var path = getIncompletePath(suggestedName); - await CreatePortablePackage(progress, packDirectory, path); - }); - - // TODO: hack, this is a prerequisite for building full package but only on linux - if (VelopackRuntimeInfo.IsLinux) await portableTask; - - string releasePath = null; - await ctx.RunTask($"Building release {packVersion}", async (progress) => { - var suggestedName = ReleaseEntryHelper.GetSuggestedReleaseName(packId, packVersion, channel, false); - releasePath = getIncompletePath(suggestedName); - await CreateReleasePackage(progress, packDirectory, releasePath); - }); - - Task setupTask = null; - if (VelopackRuntimeInfo.IsWindows || VelopackRuntimeInfo.IsOSX) { - setupTask = ctx.RunTask("Building setup package", async (progress) => { - var suggestedName = ReleaseEntryHelper.GetSuggestedSetupName(packId, channel); - var path = getIncompletePath(suggestedName); - await CreateSetupPackage(progress, releasePath, packDirectory, path); - }); - } - - if (prev != null && options.DeltaMode != DeltaMode.None) { - await ctx.RunTask($"Building delta {prev.Version} -> {packVersion}", async (progress) => { - var suggestedName = ReleaseEntryHelper.GetSuggestedReleaseName(packId, packVersion, channel, true); - var deltaPkg = await CreateDeltaPackage(progress, releasePath, prev.PackageFile, getIncompletePath(suggestedName), options.DeltaMode); - }); - } - - if (!VelopackRuntimeInfo.IsLinux) await portableTask; - if (setupTask != null) await setupTask; - - await ctx.RunTask("Post-process steps", (progress) => { - var expectedAssets = VelopackRuntimeInfo.IsLinux ? 2 : 3; - if (prev != null && options.DeltaMode != DeltaMode.None) expectedAssets += 1; - if (filesToCopy.Count != expectedAssets) { - throw new Exception($"Expected {expectedAssets} assets to be created, but only {filesToCopy.Count} were."); - } - - foreach (var f in filesToCopy) { - Utility.MoveFile(f.from, f.to, true); - } - - ReleaseEntryHelper.UpdateReleaseFiles(releaseDir.FullName, Log); - BuildAssets.Write(releaseDir.FullName, channel, filesToCopy.Select(x => x.to)); - progress(100); - return Task.CompletedTask; - }); + await Console.ExecuteProgressAsync(async (ctx) => { + ReleasePackage prev = null; + await ctx.RunTask("Pre-process steps", async (progress) => { + prev = entryHelper.GetPreviousFullRelease(NuGetVersion.Parse(packVersion)); + packDirectory = await PreprocessPackDir(progress, packDirectory); }); - } - protected virtual string GetRuntimeDependencies() - { - return null; - } + if (VelopackRuntimeInfo.IsWindows || VelopackRuntimeInfo.IsOSX) { + await ctx.RunTask("Code-sign application", async (progress) => { + await CodeSign(progress, packDirectory); + }); + } - protected virtual string GenerateNuspecContent() - { - var packId = Options.PackId; - var packTitle = Options.PackTitle ?? Options.PackId; - var packAuthors = Options.PackAuthors ?? Options.PackId; - var packVersion = Options.PackVersion; - var releaseNotes = Options.ReleaseNotes; - var rid = Options.TargetRuntime; + var portableTask = ctx.RunTask("Building portable package", async (progress) => { + var suggestedName = ReleaseEntryHelper.GetSuggestedPortableName(packId, channel); + var path = getIncompletePath(suggestedName); + await CreatePortablePackage(progress, packDirectory, path); + }); - string releaseNotesText = ""; - if (!String.IsNullOrEmpty(releaseNotes)) { - var markdown = File.ReadAllText(releaseNotes); - var html = Markdown.ToHtml(markdown); - releaseNotesText = $""" + // TODO: hack, this is a prerequisite for building full package but only on linux + if (VelopackRuntimeInfo.IsLinux) await portableTask; + + string releasePath = null; + await ctx.RunTask($"Building release {packVersion}", async (progress) => { + var suggestedName = ReleaseEntryHelper.GetSuggestedReleaseName(packId, packVersion, channel, false); + releasePath = getIncompletePath(suggestedName); + await CreateReleasePackage(progress, packDirectory, releasePath); + }); + + Task setupTask = null; + if (VelopackRuntimeInfo.IsWindows || VelopackRuntimeInfo.IsOSX) { + setupTask = ctx.RunTask("Building setup package", async (progress) => { + var suggestedName = ReleaseEntryHelper.GetSuggestedSetupName(packId, channel); + var path = getIncompletePath(suggestedName); + await CreateSetupPackage(progress, releasePath, packDirectory, path); + }); + } + + if (prev != null && options.DeltaMode != DeltaMode.None) { + await ctx.RunTask($"Building delta {prev.Version} -> {packVersion}", async (progress) => { + var suggestedName = ReleaseEntryHelper.GetSuggestedReleaseName(packId, packVersion, channel, true); + var deltaPkg = await CreateDeltaPackage(progress, releasePath, prev.PackageFile, getIncompletePath(suggestedName), options.DeltaMode); + }); + } + + if (!VelopackRuntimeInfo.IsLinux) await portableTask; + if (setupTask != null) await setupTask; + + await ctx.RunTask("Post-process steps", (progress) => { + var expectedAssets = VelopackRuntimeInfo.IsLinux ? 2 : 3; + if (prev != null && options.DeltaMode != DeltaMode.None) expectedAssets += 1; + if (filesToCopy.Count != expectedAssets) { + throw new Exception($"Expected {expectedAssets} assets to be created, but only {filesToCopy.Count} were."); + } + + foreach (var f in filesToCopy) { + Utility.MoveFile(f.from, f.to, true); + } + + ReleaseEntryHelper.UpdateReleaseFiles(releaseDir.FullName, Log); + BuildAssets.Write(releaseDir.FullName, channel, filesToCopy.Select(x => x.to)); + progress(100); + return Task.CompletedTask; + }); + }); + } + + protected virtual string GetRuntimeDependencies() + { + return null; + } + + protected virtual string GenerateNuspecContent() + { + var packId = Options.PackId; + var packTitle = Options.PackTitle ?? Options.PackId; + var packAuthors = Options.PackAuthors ?? Options.PackId; + var packVersion = Options.PackVersion; + var releaseNotes = Options.ReleaseNotes; + var rid = Options.TargetRuntime; + + string releaseNotesText = ""; + if (!String.IsNullOrEmpty(releaseNotes)) { + var markdown = File.ReadAllText(releaseNotes); + var html = Markdown.ToHtml(markdown); + releaseNotesText = $""" {SecurityElement.Escape(markdown)} """; - } + } - string osMinVersionText = ""; - if (rid?.HasVersion == true) { - osMinVersionText = $"{rid.Version}"; - } + string osMinVersionText = ""; + if (rid?.HasVersion == true) { + osMinVersionText = $"{rid.Version}"; + } - string machineArchitectureText = ""; - if (rid?.HasArchitecture == true) { - machineArchitectureText = $"{rid.Architecture}"; - } + string machineArchitectureText = ""; + if (rid?.HasArchitecture == true) { + machineArchitectureText = $"{rid.Architecture}"; + } - string runtimeDependenciesText = ""; - if (!String.IsNullOrWhiteSpace(RuntimeDependencies)) { - runtimeDependenciesText = $"{RuntimeDependencies}"; - } + string runtimeDependenciesText = ""; + if (!String.IsNullOrWhiteSpace(RuntimeDependencies)) { + runtimeDependenciesText = $"{RuntimeDependencies}"; + } - string nuspec = $""" + string nuspec = $""" @@ -227,93 +227,93 @@ namespace Velopack.Packaging """.Trim(); - return nuspec; + return nuspec; + } + + protected abstract Task PreprocessPackDir(Action progress, string packDir); + + protected virtual Task CodeSign(Action progress, string packDir) + { + return Task.CompletedTask; + } + + protected abstract Task CreatePortablePackage(Action progress, string packDir, string outputPath); + + protected virtual Task CreateDeltaPackage(Action progress, string releasePkg, string prevReleasePkg, string outputPath, DeltaMode mode) + { + var deltaBuilder = new DeltaPackageBuilder(Log); + var (dp, stats) = deltaBuilder.CreateDeltaPackage(new ReleasePackage(prevReleasePkg), new ReleasePackage(releasePkg), outputPath, mode, progress); + return Task.FromResult(dp.PackageFile); + } + + protected virtual Task CreateSetupPackage(Action progress, string releasePkg, string packDir, string outputPath) + { + return Task.CompletedTask; + } + + protected virtual async Task CreateReleasePackage(Action progress, string packDir, string outputPath) + { + var stagingDir = TempDir.CreateSubdirectory("CreateReleasePackage"); + + var nuspecPath = Path.Combine(stagingDir.FullName, Options.PackId + ".nuspec"); + File.WriteAllText(nuspecPath, GenerateNuspecContent()); + + var appDir = stagingDir.CreateSubdirectory("lib").CreateSubdirectory("app"); + CopyFiles(new DirectoryInfo(packDir), appDir, Utility.CreateProgressDelegate(progress, 0, 30)); + + var metadataFiles = GetReleaseMetadataFiles(); + foreach (var kvp in metadataFiles) { + File.Copy(kvp.Value, Path.Combine(stagingDir.FullName, kvp.Key), true); } - protected abstract Task PreprocessPackDir(Action progress, string packDir); + AddContentTypesAndRel(nuspecPath); - protected virtual Task CodeSign(Action progress, string packDir) + await EasyZip.CreateZipFromDirectoryAsync(Log, outputPath, stagingDir.FullName, Utility.CreateProgressDelegate(progress, 30, 100)); + progress(100); + } + + protected virtual Dictionary GetReleaseMetadataFiles() + { + return new Dictionary(); + } + + protected virtual void CopyFiles(DirectoryInfo source, DirectoryInfo target, Action progress, bool excludeAnnoyances = false) + { + var numFiles = source.EnumerateFiles("*", SearchOption.AllDirectories).Count(); + int currentFile = 0; + + void CopyFilesInternal(DirectoryInfo source, DirectoryInfo target) { - return Task.CompletedTask; - } - - protected abstract Task CreatePortablePackage(Action progress, string packDir, string outputPath); - - protected virtual Task CreateDeltaPackage(Action progress, string releasePkg, string prevReleasePkg, string outputPath, DeltaMode mode) - { - var deltaBuilder = new DeltaPackageBuilder(Log); - var (dp, stats) = deltaBuilder.CreateDeltaPackage(new ReleasePackage(prevReleasePkg), new ReleasePackage(releasePkg), outputPath, mode, progress); - return Task.FromResult(dp.PackageFile); - } - - protected virtual Task CreateSetupPackage(Action progress, string releasePkg, string packDir, string outputPath) - { - return Task.CompletedTask; - } - - protected virtual async Task CreateReleasePackage(Action progress, string packDir, string outputPath) - { - var stagingDir = TempDir.CreateSubdirectory("CreateReleasePackage"); - - var nuspecPath = Path.Combine(stagingDir.FullName, Options.PackId + ".nuspec"); - File.WriteAllText(nuspecPath, GenerateNuspecContent()); - - var appDir = stagingDir.CreateSubdirectory("lib").CreateSubdirectory("app"); - CopyFiles(new DirectoryInfo(packDir), appDir, Utility.CreateProgressDelegate(progress, 0, 30)); - - var metadataFiles = GetReleaseMetadataFiles(); - foreach (var kvp in metadataFiles) { - File.Copy(kvp.Value, Path.Combine(stagingDir.FullName, kvp.Key), true); + foreach (var fileInfo in source.GetFiles()) { + var path = Path.Combine(target.FullName, fileInfo.Name); + currentFile++; + progress((int) ((double) currentFile / numFiles * 100)); + if (excludeAnnoyances && REGEX_EXCLUDES.IsMatch(path)) { + Log.Debug("Skipping because matched exclude pattern: " + path); + continue; + } + fileInfo.CopyTo(path, true); } - AddContentTypesAndRel(nuspecPath); - - await EasyZip.CreateZipFromDirectoryAsync(Log, outputPath, stagingDir.FullName, Utility.CreateProgressDelegate(progress, 30, 100)); - progress(100); - } - - protected virtual Dictionary GetReleaseMetadataFiles() - { - return new Dictionary(); - } - - protected virtual void CopyFiles(DirectoryInfo source, DirectoryInfo target, Action progress, bool excludeAnnoyances = false) - { - var numFiles = source.EnumerateFiles("*", SearchOption.AllDirectories).Count(); - int currentFile = 0; - - void CopyFilesInternal(DirectoryInfo source, DirectoryInfo target) - { - foreach (var fileInfo in source.GetFiles()) { - var path = Path.Combine(target.FullName, fileInfo.Name); - currentFile++; - progress((int) ((double) currentFile / numFiles * 100)); - if (excludeAnnoyances && REGEX_EXCLUDES.IsMatch(path)) { - Log.Debug("Skipping because matched exclude pattern: " + path); - continue; - } - fileInfo.CopyTo(path, true); - } - - foreach (var sourceSubDir in source.GetDirectories()) { - var targetSubDir = target.CreateSubdirectory(sourceSubDir.Name); - CopyFilesInternal(sourceSubDir, targetSubDir); - } + foreach (var sourceSubDir in source.GetDirectories()) { + var targetSubDir = target.CreateSubdirectory(sourceSubDir.Name); + CopyFilesInternal(sourceSubDir, targetSubDir); } - - CopyFilesInternal(source, target); } - protected virtual void AddContentTypesAndRel(string nuspecPath) - { - var rootDirectory = Path.GetDirectoryName(nuspecPath); - var extensions = Directory.EnumerateFiles(rootDirectory, "*", SearchOption.AllDirectories) - .Select(p => Path.GetExtension(p).TrimStart('.').ToLower()) - .Distinct() - .Select(ext => $""" """) - .ToArray(); + CopyFilesInternal(source, target); + } - var contentType = $""" + protected virtual void AddContentTypesAndRel(string nuspecPath) + { + var rootDirectory = Path.GetDirectoryName(nuspecPath); + var extensions = Directory.EnumerateFiles(rootDirectory, "*", SearchOption.AllDirectories) + .Select(p => Path.GetExtension(p).TrimStart('.').ToLower()) + .Distinct() + .Select(ext => $""" """) + .ToArray(); + + var contentType = $""" @@ -321,18 +321,17 @@ namespace Velopack.Packaging """; - File.WriteAllText(Path.Combine(rootDirectory, NugetUtil.ContentTypeFileName), contentType); + File.WriteAllText(Path.Combine(rootDirectory, NugetUtil.ContentTypeFileName), contentType); - var relsDir = Path.Combine(rootDirectory, "_rels"); - Directory.CreateDirectory(relsDir); + var relsDir = Path.Combine(rootDirectory, "_rels"); + Directory.CreateDirectory(relsDir); - var rels = $""" + var rels = $""" """; - File.WriteAllText(Path.Combine(relsDir, ".rels"), rels); - } + File.WriteAllText(Path.Combine(relsDir, ".rels"), rels); } } diff --git a/src/Velopack.Packaging/ReleaseEntryHelper.cs b/src/Velopack.Packaging/ReleaseEntryHelper.cs index 2ec7ca2a..3017f7f5 100644 --- a/src/Velopack.Packaging/ReleaseEntryHelper.cs +++ b/src/Velopack.Packaging/ReleaseEntryHelper.cs @@ -4,252 +4,251 @@ using Velopack.Json; using Velopack.NuGet; using Velopack.Packaging.Exceptions; -namespace Velopack.Packaging +namespace Velopack.Packaging; + +public class ReleaseEntryHelper { - public class ReleaseEntryHelper + private readonly string _outputDir; + private readonly ILogger _logger; + private readonly string _channel; + private Dictionary> _releases; + + public ReleaseEntryHelper(string outputDir, string channel, ILogger logger) { - private readonly string _outputDir; - private readonly ILogger _logger; - private readonly string _channel; - private Dictionary> _releases; + _outputDir = outputDir; + _logger = logger; + _channel = channel ?? GetDefaultChannel(); + _releases = GetReleasesFromDir(outputDir); + } - public ReleaseEntryHelper(string outputDir, string channel, ILogger logger) - { - _outputDir = outputDir; - _logger = logger; - _channel = channel ?? GetDefaultChannel(); - _releases = GetReleasesFromDir(outputDir); + private static Dictionary> GetReleasesFromDir(string dir) + { + var rel = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var releaseFile in Directory.EnumerateFiles(dir, "*.nupkg")) { + var zip = new ZipPackage(releaseFile); + var ch = zip.Channel ?? GetDefaultChannel(VelopackRuntimeInfo.SystemOs); + if (!rel.ContainsKey(ch)) + rel[ch] = new List(); + rel[ch].Add(VelopackAsset.FromZipPackage(zip)); } + return rel; + } - private static Dictionary> GetReleasesFromDir(string dir) - { - var rel = new Dictionary>(StringComparer.OrdinalIgnoreCase); - foreach (var releaseFile in Directory.EnumerateFiles(dir, "*.nupkg")) { - var zip = new ZipPackage(releaseFile); - var ch = zip.Channel ?? GetDefaultChannel(VelopackRuntimeInfo.SystemOs); - if (!rel.ContainsKey(ch)) - rel[ch] = new List(); - rel[ch].Add(VelopackAsset.FromZipPackage(zip)); - } - return rel; - } - - public bool DoesSimilarVersionExist(SemanticVersion version) - { - if (!_releases.ContainsKey(_channel) || !_releases[_channel].Any()) - return false; - foreach (var release in _releases[_channel]) { - if (version <= release.Version) { - return true; - } - } + public bool DoesSimilarVersionExist(SemanticVersion version) + { + if (!_releases.ContainsKey(_channel) || !_releases[_channel].Any()) return false; + foreach (var release in _releases[_channel]) { + if (version <= release.Version) { + return true; + } + } + return false; + } + + public ReleasePackage GetPreviousFullRelease(SemanticVersion version) + { + var releases = _releases.ContainsKey(_channel) ? _releases[_channel] : null; + if (releases == null || !releases.Any()) return null; + var entry = releases + .Where(x => x.Type == VelopackAssetType.Full) + .Where(x => x.Version < version) + .OrderByDescending(x => x.Version) + .FirstOrDefault(); + if (entry == null) return null; + var file = Path.Combine(_outputDir, entry.FileName); + return new ReleasePackage(file); + } + + public VelopackAsset GetLatestFullRelease() + { + var releases = _releases.ContainsKey(_channel) ? _releases[_channel] : null; + if (releases == null || !releases.Any()) return null; + return releases.Where(z => z.Type == VelopackAssetType.Full).MaxBy(z => z.Version).First(); + } + + public IEnumerable GetLatestAssets() + { + if (!_releases.ContainsKey(_channel) || !_releases[_channel].Any()) + return Enumerable.Empty(); + + var latest = _releases[_channel].MaxBy(x => x.Version).First(); + _logger.Info($"Latest release: {latest.FileName}"); + + var assets = _releases[_channel] + .Where(x => x.Version == latest.Version) + .OrderByDescending(x => x.Version) + .ThenBy(x => x.Type) + .ToArray(); + + foreach (var asset in assets) { + _logger.Info($" Discovered asset: {asset.FileName}"); } - public ReleasePackage GetPreviousFullRelease(SemanticVersion version) - { - var releases = _releases.ContainsKey(_channel) ? _releases[_channel] : null; - if (releases == null || !releases.Any()) return null; - var entry = releases - .Where(x => x.Type == VelopackAssetType.Full) - .Where(x => x.Version < version) - .OrderByDescending(x => x.Version) - .FirstOrDefault(); - if (entry == null) return null; - var file = Path.Combine(_outputDir, entry.FileName); - return new ReleasePackage(file); + return assets; + } + + public static void UpdateReleaseFiles(string outputDir, ILogger log) + { + var releases = GetReleasesFromDir(outputDir); + foreach (var releaseFile in Directory.EnumerateFiles(outputDir, "RELEASES*")) { + File.Delete(releaseFile); } - - public VelopackAsset GetLatestFullRelease() - { - var releases = _releases.ContainsKey(_channel) ? _releases[_channel] : null; - if (releases == null || !releases.Any()) return null; - return releases.Where(z => z.Type == VelopackAssetType.Full).MaxBy(z => z.Version).First(); - } - - public IEnumerable GetLatestAssets() - { - if (!_releases.ContainsKey(_channel) || !_releases[_channel].Any()) - return Enumerable.Empty(); - - var latest = _releases[_channel].MaxBy(x => x.Version).First(); - _logger.Info($"Latest release: {latest.FileName}"); - - var assets = _releases[_channel] - .Where(x => x.Version == latest.Version) - .OrderByDescending(x => x.Version) - .ThenBy(x => x.Type) - .ToArray(); - - foreach (var asset in assets) { - _logger.Info($" Discovered asset: {asset.FileName}"); + foreach (var kvp in releases) { + var exclude = kvp.Value.Where(x => x.Version.ReleaseLabels.Any(r => r.Contains('.')) || x.Version.HasMetadata).ToArray(); + if (exclude.Any()) { + log.Warn($"Excluding {exclude.Length} assets from legacy RELEASES file, because they " + + $"contain an invalid character in the version: {string.Join(", ", exclude.Select(x => x.FileName))}"); } - return assets; - } - - public static void UpdateReleaseFiles(string outputDir, ILogger log) - { - var releases = GetReleasesFromDir(outputDir); - foreach (var releaseFile in Directory.EnumerateFiles(outputDir, "RELEASES*")) { - File.Delete(releaseFile); - } - foreach (var kvp in releases) { - var exclude = kvp.Value.Where(x => x.Version.ReleaseLabels.Any(r => r.Contains('.')) || x.Version.HasMetadata).ToArray(); - if (exclude.Any()) { - log.Warn($"Excluding {exclude.Length} assets from legacy RELEASES file, because they " + - $"contain an invalid character in the version: {string.Join(", ", exclude.Select(x => x.FileName))}"); - } - - // We write a legacy RELEASES file to allow older applications to update to velopack + // We write a legacy RELEASES file to allow older applications to update to velopack #pragma warning disable CS0612 // Type or member is obsolete #pragma warning disable CS0618 // Type or member is obsolete - var name = Utility.GetReleasesFileName(kvp.Key); - var path = Path.Combine(outputDir, name); - ReleaseEntry.WriteReleaseFile(kvp.Value.Except(exclude).Select(ReleaseEntry.FromVelopackAsset), path); + var name = Utility.GetReleasesFileName(kvp.Key); + var path = Path.Combine(outputDir, name); + ReleaseEntry.WriteReleaseFile(kvp.Value.Except(exclude).Select(ReleaseEntry.FromVelopackAsset), path); #pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0612 // Type or member is obsolete - var indexPath = Path.Combine(outputDir, Utility.GetVeloReleaseIndexName(kvp.Key)); - var feed = new VelopackAssetFeed() { - Assets = kvp.Value.OrderByDescending(v => v.Version).ThenBy(v => v.Type).ToArray(), - }; - File.WriteAllText(indexPath, GetAssetFeedJson(feed)); - } + var indexPath = Path.Combine(outputDir, Utility.GetVeloReleaseIndexName(kvp.Key)); + var feed = new VelopackAssetFeed() { + Assets = kvp.Value.OrderByDescending(v => v.Version).ThenBy(v => v.Type).ToArray(), + }; + File.WriteAllText(indexPath, GetAssetFeedJson(feed)); } - - public static IEnumerable MergeAssets(IEnumerable priority, IEnumerable secondary) - { -#if NET6_0_OR_GREATER - return priority.Concat(secondary).DistinctBy(x => x.FileName); -#else - return priority.Concat(secondary).GroupBy(x => x.FileName).Select(g => g.First()); -#endif - } - - public static string GetAssetFeedJson(VelopackAssetFeed feed) - { - return SimpleJson.SerializeObject(feed); - } - - public static string GetSuggestedReleaseName(string id, string version, string channel, bool delta) - { - var suffix = GetUniqueAssetSuffix(channel); - version = SemanticVersion.Parse(version).ToNormalizedString(); - if (VelopackRuntimeInfo.IsWindows && channel == GetDefaultChannel(RuntimeOs.Windows)) { - return $"{id}-{version}{(delta ? "-delta" : "-full")}.nupkg"; - } - return $"{id}-{version}{suffix}{(delta ? "-delta" : "-full")}.nupkg"; - } - - public static string GetSuggestedPortableName(string id, string channel) - { - var suffix = GetUniqueAssetSuffix(channel); - if (VelopackRuntimeInfo.IsLinux) { - if (channel == GetDefaultChannel(RuntimeOs.Linux)) { - return $"{id}.AppImage"; - } else { - return $"{id}{suffix}.AppImage"; - } - } else { - return $"{id}{suffix}-Portable.zip"; - } - } - - public static string GetSuggestedSetupName(string id, string channel) - { - var suffix = GetUniqueAssetSuffix(channel); - if (VelopackRuntimeInfo.IsWindows) - return $"{id}{suffix}-Setup.exe"; - else if (VelopackRuntimeInfo.IsOSX) - return $"{id}{suffix}-Setup.pkg"; - else - throw new PlatformNotSupportedException("Platform not supported."); - } - - private static string GetUniqueAssetSuffix(string channel) - { - return "-" + channel; - } - - public static string GetDefaultChannel(RuntimeOs? os = null) - { - os ??= VelopackRuntimeInfo.SystemOs; - if (os == RuntimeOs.Windows) return "win"; - if (os == RuntimeOs.OSX) return "osx"; - if (os == RuntimeOs.Linux) return "linux"; - throw new NotSupportedException("Unsupported OS: " + os); - } - - public enum AssetsMode - { - AllPackages, - OnlyLatest, - } - - //public class AssetUploadInfo - //{ - // public List Files { get; } = new List(); - - // public List Releases { get; } = new List(); - //} - - //public static AssetUploadInfo GetUploadAssets(AssetsMode mode, string channel, string releasesDir) - //{ - // var ret = new AssetUploadInfo(); - // var os = VelopackRuntimeInfo.SystemOs; - // channel ??= GetDefaultChannel(os); - // var suffix = GetPkgSuffix(os, channel); - - // if (!_releases.ContainsKey(channel)) - // throw new UserInfoException("No releases found for channel: " + channel); - - // ret.ReleasesFileName = Utility.GetReleasesFileName(channel); - // var relPath = GetReleasePath(channel); - // if (!File.Exists(relPath)) - // throw new UserInfoException($"Could not find RELEASES file for channel {channel} at {relPath}"); - - // ReleaseEntry latest = GetLatestFullRelease(channel); - // if (latest == null) { - // throw new UserInfoException("No full releases found for channel: " + channel); - // } else { - // _logger.Info("Latest local release: " + latest.OriginalFilename); - // } - - // foreach (var rel in Directory.EnumerateFiles(_outputDir, "*.nupkg")) { - // var entry = _releases[channel].FirstOrDefault(x => Path.GetFileName(rel).Equals(x.OriginalFilename, StringComparison.OrdinalIgnoreCase)); - // if (entry != null) { - // if (mode != AssetsMode.OnlyLatest || latest.Version == entry.Version) { - // _logger.Info($"Discovered asset: {rel}"); - // ret.Files.Add(new FileInfo(rel)); - // ret.Releases.Add(entry); - // } - // } - // } - - // foreach (var rel in Directory.EnumerateFiles(_outputDir, $"*{suffix}-Portable.zip")) { - // _logger.Info($"Discovered asset: {rel}"); - // ret.Files.Add(new FileInfo(rel)); - // } - - // foreach (var rel in Directory.EnumerateFiles(_outputDir, $"*{suffix}.AppImage")) { - // _logger.Info($"Discovered asset: {rel}"); - // ret.Files.Add(new FileInfo(rel)); - // } - - // foreach (var rel in Directory.EnumerateFiles(_outputDir, $"*{suffix}-Setup.exe")) { - // _logger.Info($"Discovered asset: {rel}"); - // ret.Files.Add(new FileInfo(rel)); - // } - - // foreach (var rel in Directory.EnumerateFiles(_outputDir, $"*{suffix}-Setup.pkg")) { - // _logger.Info($"Discovered asset: {rel}"); - // ret.Files.Add(new FileInfo(rel)); - // } - - // return ret; - //} - - } + + public static IEnumerable MergeAssets(IEnumerable priority, IEnumerable secondary) + { +#if NET6_0_OR_GREATER + return priority.Concat(secondary).DistinctBy(x => x.FileName); +#else + return priority.Concat(secondary).GroupBy(x => x.FileName).Select(g => g.First()); +#endif + } + + public static string GetAssetFeedJson(VelopackAssetFeed feed) + { + return SimpleJson.SerializeObject(feed); + } + + public static string GetSuggestedReleaseName(string id, string version, string channel, bool delta) + { + var suffix = GetUniqueAssetSuffix(channel); + version = SemanticVersion.Parse(version).ToNormalizedString(); + if (VelopackRuntimeInfo.IsWindows && channel == GetDefaultChannel(RuntimeOs.Windows)) { + return $"{id}-{version}{(delta ? "-delta" : "-full")}.nupkg"; + } + return $"{id}-{version}{suffix}{(delta ? "-delta" : "-full")}.nupkg"; + } + + public static string GetSuggestedPortableName(string id, string channel) + { + var suffix = GetUniqueAssetSuffix(channel); + if (VelopackRuntimeInfo.IsLinux) { + if (channel == GetDefaultChannel(RuntimeOs.Linux)) { + return $"{id}.AppImage"; + } else { + return $"{id}{suffix}.AppImage"; + } + } else { + return $"{id}{suffix}-Portable.zip"; + } + } + + public static string GetSuggestedSetupName(string id, string channel) + { + var suffix = GetUniqueAssetSuffix(channel); + if (VelopackRuntimeInfo.IsWindows) + return $"{id}{suffix}-Setup.exe"; + else if (VelopackRuntimeInfo.IsOSX) + return $"{id}{suffix}-Setup.pkg"; + else + throw new PlatformNotSupportedException("Platform not supported."); + } + + private static string GetUniqueAssetSuffix(string channel) + { + return "-" + channel; + } + + public static string GetDefaultChannel(RuntimeOs? os = null) + { + os ??= VelopackRuntimeInfo.SystemOs; + if (os == RuntimeOs.Windows) return "win"; + if (os == RuntimeOs.OSX) return "osx"; + if (os == RuntimeOs.Linux) return "linux"; + throw new NotSupportedException("Unsupported OS: " + os); + } + + public enum AssetsMode + { + AllPackages, + OnlyLatest, + } + + //public class AssetUploadInfo + //{ + // public List Files { get; } = new List(); + + // public List Releases { get; } = new List(); + //} + + //public static AssetUploadInfo GetUploadAssets(AssetsMode mode, string channel, string releasesDir) + //{ + // var ret = new AssetUploadInfo(); + // var os = VelopackRuntimeInfo.SystemOs; + // channel ??= GetDefaultChannel(os); + // var suffix = GetPkgSuffix(os, channel); + + // if (!_releases.ContainsKey(channel)) + // throw new UserInfoException("No releases found for channel: " + channel); + + // ret.ReleasesFileName = Utility.GetReleasesFileName(channel); + // var relPath = GetReleasePath(channel); + // if (!File.Exists(relPath)) + // throw new UserInfoException($"Could not find RELEASES file for channel {channel} at {relPath}"); + + // ReleaseEntry latest = GetLatestFullRelease(channel); + // if (latest == null) { + // throw new UserInfoException("No full releases found for channel: " + channel); + // } else { + // _logger.Info("Latest local release: " + latest.OriginalFilename); + // } + + // foreach (var rel in Directory.EnumerateFiles(_outputDir, "*.nupkg")) { + // var entry = _releases[channel].FirstOrDefault(x => Path.GetFileName(rel).Equals(x.OriginalFilename, StringComparison.OrdinalIgnoreCase)); + // if (entry != null) { + // if (mode != AssetsMode.OnlyLatest || latest.Version == entry.Version) { + // _logger.Info($"Discovered asset: {rel}"); + // ret.Files.Add(new FileInfo(rel)); + // ret.Releases.Add(entry); + // } + // } + // } + + // foreach (var rel in Directory.EnumerateFiles(_outputDir, $"*{suffix}-Portable.zip")) { + // _logger.Info($"Discovered asset: {rel}"); + // ret.Files.Add(new FileInfo(rel)); + // } + + // foreach (var rel in Directory.EnumerateFiles(_outputDir, $"*{suffix}.AppImage")) { + // _logger.Info($"Discovered asset: {rel}"); + // ret.Files.Add(new FileInfo(rel)); + // } + + // foreach (var rel in Directory.EnumerateFiles(_outputDir, $"*{suffix}-Setup.exe")) { + // _logger.Info($"Discovered asset: {rel}"); + // ret.Files.Add(new FileInfo(rel)); + // } + + // foreach (var rel in Directory.EnumerateFiles(_outputDir, $"*{suffix}-Setup.pkg")) { + // _logger.Info($"Discovered asset: {rel}"); + // ret.Files.Add(new FileInfo(rel)); + // } + + // return ret; + //} + + } diff --git a/src/Velopack.Vpk/Commands/DeltaGenCommand.cs b/src/Velopack.Vpk/Commands/DeltaGenCommand.cs index 15a6d907..d3b354bf 100644 --- a/src/Velopack.Vpk/Commands/DeltaGenCommand.cs +++ b/src/Velopack.Vpk/Commands/DeltaGenCommand.cs @@ -1,43 +1,42 @@ using Velopack.Packaging; -namespace Velopack.Vpk.Commands +namespace Velopack.Vpk.Commands; + +public class DeltaGenCommand : BaseCommand { - public class DeltaGenCommand : BaseCommand + public DeltaMode DeltaMode { get; set; } + + public string BasePackage { get; set; } + + public string NewPackage { get; set; } + + public string OutputFile { get; set; } + + public DeltaGenCommand() + : base("generate", "Generate a delta patch from two full releases.") + { - public DeltaMode DeltaMode { get; set; } + AddOption((v) => DeltaMode = v, "--mode") + .SetDefault(DeltaMode.BestSpeed) + .SetDescription("Set the delta generation mode."); - public string BasePackage { get; set; } + AddOption((v) => BasePackage = v.ToFullNameOrNull(), "--base", "-b") + .SetDescription("The base package for the created patch.") + .SetArgumentHelpName("PATH") + .RequiresExtension(".nupkg") + .MustExist() + .SetRequired(); - public string NewPackage { get; set; } + AddOption((v) => NewPackage = v.ToFullNameOrNull(), "--new", "-n") + .SetDescription("The resulting package for the created patch.") + .SetArgumentHelpName("PATH") + .RequiresExtension(".nupkg") + .MustExist() + .SetRequired(); - public string OutputFile { get; set; } - - public DeltaGenCommand() - : base("generate", "Generate a delta patch from two full releases.") - - { - AddOption((v) => DeltaMode = v, "--mode") - .SetDefault(DeltaMode.BestSpeed) - .SetDescription("Set the delta generation mode."); - - AddOption((v) => BasePackage = v.ToFullNameOrNull(), "--base", "-b") - .SetDescription("The base package for the created patch.") - .SetArgumentHelpName("PATH") - .RequiresExtension(".nupkg") - .MustExist() - .SetRequired(); - - AddOption((v) => NewPackage = v.ToFullNameOrNull(), "--new", "-n") - .SetDescription("The resulting package for the created patch.") - .SetArgumentHelpName("PATH") - .RequiresExtension(".nupkg") - .MustExist() - .SetRequired(); - - AddOption((v) => OutputFile = v.ToFullNameOrNull(), "--output", "-o") - .SetDescription("The output file path for the created patch.") - .SetArgumentHelpName("PATH") - .SetRequired(); - } + AddOption((v) => OutputFile = v.ToFullNameOrNull(), "--output", "-o") + .SetDescription("The output file path for the created patch.") + .SetArgumentHelpName("PATH") + .SetRequired(); } } diff --git a/src/Velopack.Vpk/Commands/DeltaPatchCommand.cs b/src/Velopack.Vpk/Commands/DeltaPatchCommand.cs index 705c66cc..d7b86fb1 100644 --- a/src/Velopack.Vpk/Commands/DeltaPatchCommand.cs +++ b/src/Velopack.Vpk/Commands/DeltaPatchCommand.cs @@ -1,33 +1,32 @@ -namespace Velopack.Vpk.Commands +namespace Velopack.Vpk.Commands; + +public class DeltaPatchCommand : BaseCommand { - public class DeltaPatchCommand : BaseCommand + public string BasePackage { get; set; } + + public FileInfo[] PatchFiles { get; set; } + + public string OutputFile { get; set; } + + public DeltaPatchCommand() + : base("patch", "Patch a base package and retrieve the original new package.") + { - public string BasePackage { get; set; } + AddOption((v) => BasePackage = v.ToFullNameOrNull(), "--base", "-b") + .SetDescription("The base package for the created patch.") + .SetArgumentHelpName("PATH") + .RequiresExtension(".nupkg") + .MustExist() + .SetRequired(); - public FileInfo[] PatchFiles { get; set; } + AddOption((v) => PatchFiles = v, "--patch", "-p") + .SetDescription("The resulting package for the created patch.") + .AllowMultiple() + .SetArgumentHelpName("PATH"); - public string OutputFile { get; set; } - - public DeltaPatchCommand() - : base("patch", "Patch a base package and retrieve the original new package.") - - { - AddOption((v) => BasePackage = v.ToFullNameOrNull(), "--base", "-b") - .SetDescription("The base package for the created patch.") - .SetArgumentHelpName("PATH") - .RequiresExtension(".nupkg") - .MustExist() - .SetRequired(); - - AddOption((v) => PatchFiles = v, "--patch", "-p") - .SetDescription("The resulting package for the created patch.") - .AllowMultiple() - .SetArgumentHelpName("PATH"); - - AddOption((v) => OutputFile = v.ToFullNameOrNull(), "--output", "-o") - .SetDescription("The output file path for the created patch.") - .SetArgumentHelpName("PATH") - .SetRequired(); - } + AddOption((v) => OutputFile = v.ToFullNameOrNull(), "--output", "-o") + .SetDescription("The output file path for the created patch.") + .SetArgumentHelpName("PATH") + .SetRequired(); } } diff --git a/src/Velopack.Vpk/Commands/LinuxPackCommand.cs b/src/Velopack.Vpk/Commands/LinuxPackCommand.cs index 244a6a96..79a73212 100644 --- a/src/Velopack.Vpk/Commands/LinuxPackCommand.cs +++ b/src/Velopack.Vpk/Commands/LinuxPackCommand.cs @@ -7,96 +7,95 @@ using Microsoft.NET.HostModel.Bundle; using Octokit; using Velopack.Packaging; -namespace Velopack.Vpk.Commands +namespace Velopack.Vpk.Commands; + +public class LinuxPackCommand : PlatformCommand { - public class LinuxPackCommand : PlatformCommand + public string PackId { get; private set; } + + public string PackVersion { get; private set; } + + public string PackDirectory { get; private set; } + + public string PackAuthors { get; private set; } + + public string PackTitle { get; private set; } + + public string EntryExecutableName { get; private set; } + + public string Icon { get; private set; } + + public string ReleaseNotes { get; set; } + + public bool PackIsAppDir { get; private set; } + + public DeltaMode DeltaMode { get; set; } = DeltaMode.BestSpeed; + + public LinuxPackCommand() + : this("pack", "Create's a Linux .AppImage bundle from a folder containing application files.") + { } + + public LinuxPackCommand(string name, string description) + : base(name, description) { - public string PackId { get; private set; } + AddOption((v) => PackId = v, "--packId", "-u") + .SetDescription("Unique Id for application bundle.") + .SetArgumentHelpName("ID") + .SetRequired() + .RequiresValidNuGetId(); - public string PackVersion { get; private set; } + // TODO add parser straight to SemanticVersion? + AddOption((v) => PackVersion = v, "--packVersion", "-v") + .SetDescription("Current version for application bundle.") + .SetArgumentHelpName("VERSION") + .SetRequired() + .RequiresSemverCompliant(); - public string PackDirectory { get; private set; } + var packDir = AddOption((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p") + .SetDescription("Directory containing application files from dotnet publish") + .SetArgumentHelpName("DIR") + .MustNotBeEmpty(); - public string PackAuthors { get; private set; } + AddOption((v) => PackAuthors = v, "--packAuthors") + .SetDescription("Company name or comma-delimited list of authors.") + .SetArgumentHelpName("AUTHORS"); - public string PackTitle { get; private set; } + AddOption((v) => PackTitle = v, "--packTitle") + .SetDescription("Display/friendly name for application.") + .SetArgumentHelpName("NAME"); - public string EntryExecutableName { get; private set; } + AddOption((v) => EntryExecutableName = v, "-e", "--mainExe") + .SetDescription("The file name of the main/entry executable.") + .SetArgumentHelpName("NAME"); - public string Icon { get; private set; } + var icon = AddOption((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon") + .SetDescription("Path to the icon file for this bundle.") + .SetArgumentHelpName("PATH") + .MustExist(); - public string ReleaseNotes { get; set; } + AddOption((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes") + .SetDescription("File with markdown-formatted notes for this version.") + .SetArgumentHelpName("PATH") + .MustExist(); - public bool PackIsAppDir { get; private set; } + AddOption((v) => DeltaMode = v, "--delta") + .SetDefault(DeltaMode.BestSpeed) + .SetDescription("Set the delta generation mode."); - public DeltaMode DeltaMode { get; set; } = DeltaMode.BestSpeed; + var appDir = AddOption((v) => { + var t = v.ToFullNameOrNull(); + if (t != null) { + PackDirectory = t; + PackIsAppDir = true; + } + }, "--appDir") + .SetDescription("Directory containing application in .AppDir format") + .SetArgumentHelpName("DIR") + .MustNotBeEmpty(); - public LinuxPackCommand() - : this("pack", "Create's a Linux .AppImage bundle from a folder containing application files.") - { } - - public LinuxPackCommand(string name, string description) - : base(name, description) - { - AddOption((v) => PackId = v, "--packId", "-u") - .SetDescription("Unique Id for application bundle.") - .SetArgumentHelpName("ID") - .SetRequired() - .RequiresValidNuGetId(); - - // TODO add parser straight to SemanticVersion? - AddOption((v) => PackVersion = v, "--packVersion", "-v") - .SetDescription("Current version for application bundle.") - .SetArgumentHelpName("VERSION") - .SetRequired() - .RequiresSemverCompliant(); - - var packDir = AddOption((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p") - .SetDescription("Directory containing application files from dotnet publish") - .SetArgumentHelpName("DIR") - .MustNotBeEmpty(); - - AddOption((v) => PackAuthors = v, "--packAuthors") - .SetDescription("Company name or comma-delimited list of authors.") - .SetArgumentHelpName("AUTHORS"); - - AddOption((v) => PackTitle = v, "--packTitle") - .SetDescription("Display/friendly name for application.") - .SetArgumentHelpName("NAME"); - - AddOption((v) => EntryExecutableName = v, "-e", "--mainExe") - .SetDescription("The file name of the main/entry executable.") - .SetArgumentHelpName("NAME"); - - var icon = AddOption((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon") - .SetDescription("Path to the icon file for this bundle.") - .SetArgumentHelpName("PATH") - .MustExist(); - - AddOption((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes") - .SetDescription("File with markdown-formatted notes for this version.") - .SetArgumentHelpName("PATH") - .MustExist(); - - AddOption((v) => DeltaMode = v, "--delta") - .SetDefault(DeltaMode.BestSpeed) - .SetDescription("Set the delta generation mode."); - - var appDir = AddOption((v) => { - var t = v.ToFullNameOrNull(); - if (t != null) { - PackDirectory = t; - PackIsAppDir = true; - } - }, "--appDir") - .SetDescription("Directory containing application in .AppDir format") - .SetArgumentHelpName("DIR") - .MustNotBeEmpty(); - - this.AreMutuallyExclusive(packDir, appDir); - this.AtLeastOneRequired(packDir, appDir); - this.AreMutuallyExclusive(icon, appDir); - this.AtLeastOneRequired(icon, appDir); - } + this.AreMutuallyExclusive(packDir, appDir); + this.AtLeastOneRequired(packDir, appDir); + this.AreMutuallyExclusive(icon, appDir); + this.AtLeastOneRequired(icon, appDir); } } diff --git a/src/Velopack.Vpk/Commands/LongHelpCommand.cs b/src/Velopack.Vpk/Commands/LongHelpCommand.cs index d1c0748b..a744ce24 100644 --- a/src/Velopack.Vpk/Commands/LongHelpCommand.cs +++ b/src/Velopack.Vpk/Commands/LongHelpCommand.cs @@ -7,293 +7,292 @@ using Spectre.Console.Rendering; using Velopack.Vpk.Logging; using static System.CommandLine.Help.HelpBuilder; -namespace Velopack.Vpk.Commands +namespace Velopack.Vpk.Commands; + +public class LongHelpCommand : CliOption { - public class LongHelpCommand : CliOption + private CliAction _action; + + public LongHelpCommand() : this("--help", ["-h", "-H", "--vhelp"]) { - private CliAction _action; + } - public LongHelpCommand() : this("--help", ["-h", "-H", "--vhelp"]) + public LongHelpCommand(string name, params string[] aliases) + : base(name, aliases) + { + Recursive = true; + Description = "Show help (-h) or extended help (-H)."; + Arity = ArgumentArity.Zero; + } + + public override CliAction Action { + get => _action ??= new LongHelpAction(); + set => _action = value ?? throw new ArgumentNullException(nameof(value)); + } + + public sealed class LongHelpAction : SynchronousCliAction + { + public override int Invoke(ParseResult parseResult) { - } + var longHelpMode = parseResult.Tokens.Any(t => t.Value == "-H" || t.Value == "--vhelp"); + var command = parseResult.CommandResult.Command; - public LongHelpCommand(string name, params string[] aliases) - : base(name, aliases) - { - Recursive = true; - Description = "Show help (-h) or extended help (-H)."; - Arity = ArgumentArity.Zero; - } + var pad = new Padding(2, 0); - public override CliAction Action { - get => _action ??= new LongHelpAction(); - set => _action = value ?? throw new ArgumentNullException(nameof(value)); - } + List output = + [ + new Text("Description:"), + Text.NewLine, + new Padder(new Markup($"[bold]{command.Description}[/]"), pad), + Text.NewLine, + new Text("Usage:"), + Text.NewLine, + new Padder(new Markup($"[bold]{Markup.Escape(GetUsage(command))}[/]"), pad), + ]; - public sealed class LongHelpAction : SynchronousCliAction - { - public override int Invoke(ParseResult parseResult) + Table CreateTable() { - var longHelpMode = parseResult.Tokens.Any(t => t.Value == "-H" || t.Value == "--vhelp"); - var command = parseResult.CommandResult.Command; - - var pad = new Padding(2, 0); - - List output = - [ - new Text("Description:"), - Text.NewLine, - new Padder(new Markup($"[bold]{command.Description}[/]"), pad), - Text.NewLine, - new Text("Usage:"), - Text.NewLine, - new Padder(new Markup($"[bold]{Markup.Escape(GetUsage(command))}[/]"), pad), - ]; - - Table CreateTable() - { - var t = new Table(); - t.NoBorder(); - t.LeftAligned(); - t.HideHeaders(); - t.Collapse(); - t.AddColumn(new TableColumn("Name") { Padding = pad }); - t.AddColumn("Description"); - return t; - } - - int hiddenOptions = 0; - - void AddOptionRows(Table table, IEnumerable options) - { - foreach (var argument in options) { - if (argument.Hidden && !longHelpMode) { - hiddenOptions++; - continue; - } - - var columns = GetTwoColumnRowOption(argument); - var aliasText = columns.FirstColumnText; - var argIdx = aliasText.IndexOf(" <"); - if (argIdx > 0) { - aliasText = $"[bold]{aliasText.Substring(0, argIdx)}[/]{aliasText.Substring(argIdx)}"; - } else { - aliasText = $"[bold]{aliasText}[/]"; - } - aliasText = aliasText.Replace("(REQUIRED)", "[red](REQ)[/]"); - - var descriptionText = Markup.Escape(columns.SecondColumnText); - if (longHelpMode && command is BaseCommand bc) { - var envVarName = bc.GetEnvVariableName(argument); - if (envVarName != null) { - descriptionText = $"[italic]ENV=[bold blue]{envVarName}[/][/] " + descriptionText; - } - } - table.AddRow(new Markup(aliasText), new Markup(descriptionText)); - } - } - - // look for global options (only rendered if long mode) - //var globalOptions = new List(); - //foreach (var p in command.Parents) { - // CliCommand parentCommand = null; - // if ((parentCommand = p as CliCommand) is not null) { - // if (parentCommand.HasOptions()) { - // foreach (var option in parentCommand.Options) { - // if (option is { Recursive: true, Hidden: false }) { - // if (longHelpMode) { - // globalOptions.Add(option); - // } else { - // hiddenOptions++; - // } - // } - // } - // } - // } - //} - - //if (globalOptions.Any()) { - // output.Add(Text.NewLine); - // output.Add(new Text($"Global Options:")); - // output.Add(Text.NewLine); - // var globalOptionsTable = CreateTable(); - // AddOptionRows(globalOptionsTable, globalOptions); - // output.Add(new Padder(globalOptionsTable, pad)); - //} - - if (command.HasOptions()) { - output.Add(Text.NewLine); - output.Add(new Text($"Options:")); - output.Add(Text.NewLine); - var optionsTable = CreateTable(); - AddOptionRows(optionsTable, command.Options); - output.Add(new Padder(optionsTable, pad)); - } - - if (hiddenOptions > 0) { - output.Add(Text.NewLine); - output.Add(new Markup($"[italic]([red]*[/]) {hiddenOptions} option(s) were hidden. Use [bold]-H / --vhelp[/] to show all options.[/]")); - output.Add(Text.NewLine); - } - - if (command.HasSubcommands()) { - output.Add(Text.NewLine); - output.Add(new Text("Commands:")); - output.Add(Text.NewLine); - - var commandsTable = CreateTable(); - foreach (var cmd in command.Subcommands) { - var columns = GetTwoColumnRowCommand(cmd); - commandsTable.AddRow(new Markup($"[bold]{columns.FirstColumnText}[/]"), new Text(columns.SecondColumnText)); - } - - output.Add(new Padder(commandsTable, pad)); - } - - AnsiConsole.Write(new RenderableCollection(output)); - return 0; + var t = new Table(); + t.NoBorder(); + t.LeftAligned(); + t.HideHeaders(); + t.Collapse(); + t.AddColumn(new TableColumn("Name") { Padding = pad }); + t.AddColumn("Description"); + return t; } - public TwoColumnHelpRow GetTwoColumnRowCommand(CliCommand command) + int hiddenOptions = 0; + + void AddOptionRows(Table table, IEnumerable options) { - if (command is null) { - throw new ArgumentNullException(nameof(command)); - } - var firstColumnText = Default.GetCommandUsageLabel(command); - var symbolDescription = command.Description ?? string.Empty; - var secondColumnText = symbolDescription.Trim(); - return new TwoColumnHelpRow(firstColumnText, secondColumnText); - } - - public TwoColumnHelpRow GetTwoColumnRowOption(CliOption symbol) - { - if (symbol is null) { - throw new ArgumentNullException(nameof(symbol)); - } - - var firstColumnText = Default.GetOptionUsageLabel(symbol); - var symbolDescription = symbol.Description ?? string.Empty; - - var defaultValueDescription = ""; - if (symbol.HasDefaultValue) { - // TODO: this is a hack, but the property is internal. what do you want me to do? - var argument = symbol.GetType()?.GetProperty("Argument", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(symbol) as CliArgument; - if (argument is not null) { - defaultValueDescription = $"[default: {Default.GetArgumentDefaultValue(argument)}]"; - } - } - - var secondColumnText = $"{symbolDescription} {defaultValueDescription}".Trim(); - return new TwoColumnHelpRow(firstColumnText, secondColumnText); - } - - private string GetUsage(CliCommand command) - { - return string.Join(" ", GetUsageParts().Where(x => !string.IsNullOrWhiteSpace(x))); - - IEnumerable GetUsageParts() - { - bool displayOptionTitle = false; - - IEnumerable parentCommands = - command - .RecurseWhileNotNull(c => c.Parents.OfType().FirstOrDefault()) - .Reverse(); - - foreach (var parentCommand in parentCommands) { - if (!displayOptionTitle) { - displayOptionTitle = parentCommand.HasOptions() && parentCommand.Options.Any(x => x.Recursive && !x.Hidden); - } - - yield return parentCommand.Name; - - if (parentCommand.HasArguments()) { - yield return FormatArgumentUsage(parentCommand.Arguments); - } - } - - var hasCommandWithHelp = command.HasSubcommands() && command.Subcommands.Any(x => !x.Hidden); - - if (hasCommandWithHelp) { - yield return "[command]"; - } - - displayOptionTitle = displayOptionTitle || (command.HasOptions() && command.Options.Any(x => !x.Hidden)); - - if (displayOptionTitle) { - yield return "[options]"; - } - - if (!command.TreatUnmatchedTokensAsErrors) { - yield return "[[--] ...]]"; - } - } - } - - private string FormatArgumentUsage(IList arguments) - { - var sb = new StringBuilder(arguments.Count * 100); - - var end = default(List); - - for (var i = 0; i < arguments.Count; i++) { - var argument = arguments[i]; - if (argument.Hidden) { + foreach (var argument in options) { + if (argument.Hidden && !longHelpMode) { + hiddenOptions++; continue; } - var arityIndicator = - argument.Arity.MaximumNumberOfValues > 1 - ? "..." - : ""; - - var isOptional = IsOptional(argument); - - if (isOptional) { - sb.Append($"[<{argument.Name}>{arityIndicator}"); - (end ??= new()).Add(']'); + var columns = GetTwoColumnRowOption(argument); + var aliasText = columns.FirstColumnText; + var argIdx = aliasText.IndexOf(" <"); + if (argIdx > 0) { + aliasText = $"[bold]{aliasText.Substring(0, argIdx)}[/]{aliasText.Substring(argIdx)}"; } else { - sb.Append($"<{argument.Name}>{arityIndicator}"); + aliasText = $"[bold]{aliasText}[/]"; } + aliasText = aliasText.Replace("(REQUIRED)", "[red](REQ)[/]"); - sb.Append(' '); - } - - if (sb.Length > 0) { - sb.Length--; - - if (end is { }) { - while (end.Count > 0) { - sb.Append(end[end.Count - 1]); - end.RemoveAt(end.Count - 1); + var descriptionText = Markup.Escape(columns.SecondColumnText); + if (longHelpMode && command is BaseCommand bc) { + var envVarName = bc.GetEnvVariableName(argument); + if (envVarName != null) { + descriptionText = $"[italic]ENV=[bold blue]{envVarName}[/][/] " + descriptionText; } } + table.AddRow(new Markup(aliasText), new Markup(descriptionText)); + } + } + + // look for global options (only rendered if long mode) + //var globalOptions = new List(); + //foreach (var p in command.Parents) { + // CliCommand parentCommand = null; + // if ((parentCommand = p as CliCommand) is not null) { + // if (parentCommand.HasOptions()) { + // foreach (var option in parentCommand.Options) { + // if (option is { Recursive: true, Hidden: false }) { + // if (longHelpMode) { + // globalOptions.Add(option); + // } else { + // hiddenOptions++; + // } + // } + // } + // } + // } + //} + + //if (globalOptions.Any()) { + // output.Add(Text.NewLine); + // output.Add(new Text($"Global Options:")); + // output.Add(Text.NewLine); + // var globalOptionsTable = CreateTable(); + // AddOptionRows(globalOptionsTable, globalOptions); + // output.Add(new Padder(globalOptionsTable, pad)); + //} + + if (command.HasOptions()) { + output.Add(Text.NewLine); + output.Add(new Text($"Options:")); + output.Add(Text.NewLine); + var optionsTable = CreateTable(); + AddOptionRows(optionsTable, command.Options); + output.Add(new Padder(optionsTable, pad)); + } + + if (hiddenOptions > 0) { + output.Add(Text.NewLine); + output.Add(new Markup($"[italic]([red]*[/]) {hiddenOptions} option(s) were hidden. Use [bold]-H / --vhelp[/] to show all options.[/]")); + output.Add(Text.NewLine); + } + + if (command.HasSubcommands()) { + output.Add(Text.NewLine); + output.Add(new Text("Commands:")); + output.Add(Text.NewLine); + + var commandsTable = CreateTable(); + foreach (var cmd in command.Subcommands) { + var columns = GetTwoColumnRowCommand(cmd); + commandsTable.AddRow(new Markup($"[bold]{columns.FirstColumnText}[/]"), new Text(columns.SecondColumnText)); } - return sb.ToString(); + output.Add(new Padder(commandsTable, pad)); + } - bool IsOptional(CliArgument argument) => - argument.Arity.MinimumNumberOfValues == 0; + AnsiConsole.Write(new RenderableCollection(output)); + return 0; + } + + public TwoColumnHelpRow GetTwoColumnRowCommand(CliCommand command) + { + if (command is null) { + throw new ArgumentNullException(nameof(command)); + } + var firstColumnText = Default.GetCommandUsageLabel(command); + var symbolDescription = command.Description ?? string.Empty; + var secondColumnText = symbolDescription.Trim(); + return new TwoColumnHelpRow(firstColumnText, secondColumnText); + } + + public TwoColumnHelpRow GetTwoColumnRowOption(CliOption symbol) + { + if (symbol is null) { + throw new ArgumentNullException(nameof(symbol)); + } + + var firstColumnText = Default.GetOptionUsageLabel(symbol); + var symbolDescription = symbol.Description ?? string.Empty; + + var defaultValueDescription = ""; + if (symbol.HasDefaultValue) { + // TODO: this is a hack, but the property is internal. what do you want me to do? + var argument = symbol.GetType()?.GetProperty("Argument", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(symbol) as CliArgument; + if (argument is not null) { + defaultValueDescription = $"[default: {Default.GetArgumentDefaultValue(argument)}]"; + } + } + + var secondColumnText = $"{symbolDescription} {defaultValueDescription}".Trim(); + return new TwoColumnHelpRow(firstColumnText, secondColumnText); + } + + private string GetUsage(CliCommand command) + { + return string.Join(" ", GetUsageParts().Where(x => !string.IsNullOrWhiteSpace(x))); + + IEnumerable GetUsageParts() + { + bool displayOptionTitle = false; + + IEnumerable parentCommands = + command + .RecurseWhileNotNull(c => c.Parents.OfType().FirstOrDefault()) + .Reverse(); + + foreach (var parentCommand in parentCommands) { + if (!displayOptionTitle) { + displayOptionTitle = parentCommand.HasOptions() && parentCommand.Options.Any(x => x.Recursive && !x.Hidden); + } + + yield return parentCommand.Name; + + if (parentCommand.HasArguments()) { + yield return FormatArgumentUsage(parentCommand.Arguments); + } + } + + var hasCommandWithHelp = command.HasSubcommands() && command.Subcommands.Any(x => !x.Hidden); + + if (hasCommandWithHelp) { + yield return "[command]"; + } + + displayOptionTitle = displayOptionTitle || (command.HasOptions() && command.Options.Any(x => !x.Hidden)); + + if (displayOptionTitle) { + yield return "[options]"; + } + + if (!command.TreatUnmatchedTokensAsErrors) { + yield return "[[--] ...]]"; + } } } - } - public static class HelpExtensions - { - public static bool HasArguments(this CliCommand command) => command.Arguments?.Count > 0; - public static bool HasSubcommands(this CliCommand command) => command.Subcommands?.Count > 0; - public static bool HasOptions(this CliCommand command) => command.Options?.Count > 0; - - internal static IEnumerable RecurseWhileNotNull( - this T source, - Func next) - where T : class + private string FormatArgumentUsage(IList arguments) { - while (source is not null) { - yield return source; + var sb = new StringBuilder(arguments.Count * 100); - source = next(source); + var end = default(List); + + for (var i = 0; i < arguments.Count; i++) { + var argument = arguments[i]; + if (argument.Hidden) { + continue; + } + + var arityIndicator = + argument.Arity.MaximumNumberOfValues > 1 + ? "..." + : ""; + + var isOptional = IsOptional(argument); + + if (isOptional) { + sb.Append($"[<{argument.Name}>{arityIndicator}"); + (end ??= new()).Add(']'); + } else { + sb.Append($"<{argument.Name}>{arityIndicator}"); + } + + sb.Append(' '); } + + if (sb.Length > 0) { + sb.Length--; + + if (end is { }) { + while (end.Count > 0) { + sb.Append(end[end.Count - 1]); + end.RemoveAt(end.Count - 1); + } + } + } + + return sb.ToString(); + + bool IsOptional(CliArgument argument) => + argument.Arity.MinimumNumberOfValues == 0; + } + } +} + +public static class HelpExtensions +{ + public static bool HasArguments(this CliCommand command) => command.Arguments?.Count > 0; + public static bool HasSubcommands(this CliCommand command) => command.Subcommands?.Count > 0; + public static bool HasOptions(this CliCommand command) => command.Options?.Count > 0; + + internal static IEnumerable RecurseWhileNotNull( + this T source, + Func next) + where T : class + { + while (source is not null) { + yield return source; + + source = next(source); } } } diff --git a/src/Velopack.Vpk/Commands/_OutputCommand.cs b/src/Velopack.Vpk/Commands/_OutputCommand.cs index ee853a66..6b75738f 100644 --- a/src/Velopack.Vpk/Commands/_OutputCommand.cs +++ b/src/Velopack.Vpk/Commands/_OutputCommand.cs @@ -1,37 +1,36 @@ using Velopack.Packaging; -namespace Velopack.Vpk.Commands +namespace Velopack.Vpk.Commands; + +public abstract class OutputCommand : BaseCommand { - public abstract class OutputCommand : BaseCommand + public string ReleaseDir { get; private set; } + + public string Channel { get; private set; } + + protected CliOption ReleaseDirectoryOption { get; private set; } + + protected CliOption ChannelOption { get; private set; } + + protected OutputCommand(string name, string description) + : base(name, description) { - public string ReleaseDir { get; private set; } + ReleaseDirectoryOption = AddOption((v) => ReleaseDir = v.ToFullNameOrNull(), "-o", "--outputDir") + .SetDescription("Output directory for created packages.") + .SetArgumentHelpName("DIR") + .SetDefault(new DirectoryInfo("Releases")); - public string Channel { get; private set; } + ChannelOption = AddOption((v) => Channel = v, "-c", "--channel") + .SetDescription("The channel to use for this release.") + .RequiresValidNuGetId() + .SetArgumentHelpName("NAME") + .SetDefault(ReleaseEntryHelper.GetDefaultChannel(VelopackRuntimeInfo.SystemOs)); + } - protected CliOption ReleaseDirectoryOption { get; private set; } - - protected CliOption ChannelOption { get; private set; } - - protected OutputCommand(string name, string description) - : base(name, description) - { - ReleaseDirectoryOption = AddOption((v) => ReleaseDir = v.ToFullNameOrNull(), "-o", "--outputDir") - .SetDescription("Output directory for created packages.") - .SetArgumentHelpName("DIR") - .SetDefault(new DirectoryInfo("Releases")); - - ChannelOption = AddOption((v) => Channel = v, "-c", "--channel") - .SetDescription("The channel to use for this release.") - .RequiresValidNuGetId() - .SetArgumentHelpName("NAME") - .SetDefault(ReleaseEntryHelper.GetDefaultChannel(VelopackRuntimeInfo.SystemOs)); - } - - public DirectoryInfo GetReleaseDirectory() - { - var di = new DirectoryInfo(ReleaseDir); - if (!di.Exists) di.Create(); - return di; - } + public DirectoryInfo GetReleaseDirectory() + { + var di = new DirectoryInfo(ReleaseDir); + if (!di.Exists) di.Create(); + return di; } } diff --git a/src/Velopack.Vpk/Commands/_PlatformCommand.cs b/src/Velopack.Vpk/Commands/_PlatformCommand.cs index 51979cf7..1fe88b5e 100644 --- a/src/Velopack.Vpk/Commands/_PlatformCommand.cs +++ b/src/Velopack.Vpk/Commands/_PlatformCommand.cs @@ -1,22 +1,21 @@ -namespace Velopack.Vpk.Commands +namespace Velopack.Vpk.Commands; + +public abstract class PlatformCommand : OutputCommand { - public abstract class PlatformCommand : OutputCommand + public string TargetRuntime { get; set; } + + protected CliOption TargetRuntimeOption { get; private set; } + + protected PlatformCommand(string name, string description) : base(name, description) { - public string TargetRuntime { get; set; } - - protected CliOption TargetRuntimeOption { get; private set; } - - protected PlatformCommand(string name, string description) : base(name, description) - { - TargetRuntimeOption = AddOption((v) => TargetRuntime = v, "-r", "--runtime") - .SetDescription("The target runtime to build packages for.") - .SetArgumentHelpName("RID") - .SetDefault(VelopackRuntimeInfo.SystemOs.GetOsShortName()) - .MustBeSupportedRid(); - } - - public RID GetRid() => RID.Parse(TargetRuntime ?? VelopackRuntimeInfo.SystemOs.GetOsShortName()); - - public RuntimeOs GetRuntimeOs() => GetRid().BaseRID; + TargetRuntimeOption = AddOption((v) => TargetRuntime = v, "-r", "--runtime") + .SetDescription("The target runtime to build packages for.") + .SetArgumentHelpName("RID") + .SetDefault(VelopackRuntimeInfo.SystemOs.GetOsShortName()) + .MustBeSupportedRid(); } + + public RID GetRid() => RID.Parse(TargetRuntime ?? VelopackRuntimeInfo.SystemOs.GetOsShortName()); + + public RuntimeOs GetRuntimeOs() => GetRid().BaseRID; } diff --git a/src/Velopack.Vpk/Logging/BasicConsole.cs b/src/Velopack.Vpk/Logging/BasicConsole.cs index 42f8d2c9..1af25cd6 100644 --- a/src/Velopack.Vpk/Logging/BasicConsole.cs +++ b/src/Velopack.Vpk/Logging/BasicConsole.cs @@ -1,58 +1,57 @@ using Velopack.Packaging.Abstractions; -namespace Velopack.Vpk.Logging +namespace Velopack.Vpk.Logging; + +public class BasicConsole : IFancyConsole { - public class BasicConsole : IFancyConsole + private readonly ILogger logger; + private readonly DefaultPromptValueFactory defaultFactory; + + public BasicConsole(ILogger logger, DefaultPromptValueFactory defaultFactory) { - private readonly ILogger logger; - private readonly DefaultPromptValueFactory defaultFactory; + this.logger = logger; + this.defaultFactory = defaultFactory; + } - public BasicConsole(ILogger logger, DefaultPromptValueFactory defaultFactory) + public async Task ExecuteProgressAsync(Func action) + { + var start = DateTime.UtcNow; + await action(new Progress(logger)); + logger.Info($"Finished in {DateTime.UtcNow - start}."); + } + + public Task PromptYesNo(string prompt, bool? defaultValue = null, TimeSpan? timeout = null) + { + return Task.FromResult(defaultValue ?? defaultFactory.DefaultPromptValue); + } + + public void WriteLine(string text = "") + { + Console.WriteLine(text); + } + + public void WriteTable(string tableName, IEnumerable> rows, bool hasHeaderRow = true) + { + Console.WriteLine(tableName); + foreach (var row in rows) { + Console.WriteLine(" " + String.Join(" ", row)); + } + } + + private class Progress : IFancyConsoleProgress + { + private readonly ILogger _logger; + + public Progress(ILogger logger) { - this.logger = logger; - this.defaultFactory = defaultFactory; + _logger = logger; } - public async Task ExecuteProgressAsync(Func action) + public async Task RunTask(string name, Func, Task> fn) { - var start = DateTime.UtcNow; - await action(new Progress(logger)); - logger.Info($"Finished in {DateTime.UtcNow - start}."); - } - - public Task PromptYesNo(string prompt, bool? defaultValue = null, TimeSpan? timeout = null) - { - return Task.FromResult(defaultValue ?? defaultFactory.DefaultPromptValue); - } - - public void WriteLine(string text = "") - { - Console.WriteLine(text); - } - - public void WriteTable(string tableName, IEnumerable> rows, bool hasHeaderRow = true) - { - Console.WriteLine(tableName); - foreach (var row in rows) { - Console.WriteLine(" " + String.Join(" ", row)); - } - } - - private class Progress : IFancyConsoleProgress - { - private readonly ILogger _logger; - - public Progress(ILogger logger) - { - _logger = logger; - } - - public async Task RunTask(string name, Func, Task> fn) - { - _logger.Info("Starting: " + name); - await Task.Run(() => fn(_ => { })); - _logger.Info("Complete: " + name); - } + _logger.Info("Starting: " + name); + await Task.Run(() => fn(_ => { })); + _logger.Info("Complete: " + name); } } } diff --git a/src/Velopack.Vpk/Logging/DefaultPromptValueFactory.cs b/src/Velopack.Vpk/Logging/DefaultPromptValueFactory.cs index 47de7701..b1689f47 100644 --- a/src/Velopack.Vpk/Logging/DefaultPromptValueFactory.cs +++ b/src/Velopack.Vpk/Logging/DefaultPromptValueFactory.cs @@ -4,9 +4,8 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Velopack.Vpk.Logging +namespace Velopack.Vpk.Logging; + +public record DefaultPromptValueFactory(bool DefaultPromptValue) { - public record DefaultPromptValueFactory(bool DefaultPromptValue) - { - } } diff --git a/src/Velopack.Vpk/Logging/SpectreConsole.cs b/src/Velopack.Vpk/Logging/SpectreConsole.cs index dd365d6b..a6af9262 100644 --- a/src/Velopack.Vpk/Logging/SpectreConsole.cs +++ b/src/Velopack.Vpk/Logging/SpectreConsole.cs @@ -8,137 +8,136 @@ using System.Threading.Tasks; using Spectre.Console; using Velopack.Packaging.Abstractions; -namespace Velopack.Vpk.Logging +namespace Velopack.Vpk.Logging; + +public class SpectreConsole : IFancyConsole { - public class SpectreConsole : IFancyConsole + private readonly ILogger logger; + private readonly DefaultPromptValueFactory defaultFactory; + + public SpectreConsole(ILogger logger, DefaultPromptValueFactory defaultFactory) { - private readonly ILogger logger; - private readonly DefaultPromptValueFactory defaultFactory; + this.logger = logger; + this.defaultFactory = defaultFactory; + } - public SpectreConsole(ILogger logger, DefaultPromptValueFactory defaultFactory) - { - this.logger = logger; - this.defaultFactory = defaultFactory; + public async Task ExecuteProgressAsync(Func action) + { + var start = DateTime.UtcNow; + await AnsiConsole.Progress() + .AutoRefresh(true) + .AutoClear(false) + .HideCompleted(false) + .Columns(new ProgressColumn[] { + new SpinnerColumn(), + new TaskDescriptionColumn(), + new ProgressBarColumn(), + new PercentageColumn(), + new ElapsedTimeColumn(), + }) + .StartAsync(async ctx => await action(new Progress(logger, ctx))); + logger.Info($"[bold]Finished in {DateTime.UtcNow - start}.[/]"); + } + + public async Task PromptYesNo(string prompt, bool? defaultValue = null, TimeSpan? timeout = null) + { + var def = defaultValue ?? defaultFactory.DefaultPromptValue; + var cts = new CancellationTokenSource(); + cts.CancelAfter(timeout ?? TimeSpan.FromSeconds(30)); + + try { + // all of this nonsense in CancellableTextPrompt.cs is to work-around a bug in Spectre. + // Once the following issue is merged it can be removed. + // https://github.com/spectreconsole/spectre.console/pull/1439 + + AnsiConsole.WriteLine(); + var comparer = StringComparer.CurrentCultureIgnoreCase; + var textPrompt = "[underline bold orange3]QUESTION:[/]" + Environment.NewLine + prompt; + var clip = new CancellableTextPrompt(textPrompt, comparer); + clip.Choices.Add('y'); + clip.Choices.Add('n'); + clip.ShowChoices = true; + clip.ShowDefaultValue = true; + clip.DefaultValue = new DefaultPromptValue(def ? 'y' : 'n'); + var result = await clip.ShowAsync(AnsiConsole.Console, cts.Token).ConfigureAwait(false); + AnsiConsole.WriteLine(); + return comparer.Compare("y", result.ToString()) == 0; + } catch (OperationCanceledException) { + AnsiConsole.Write($" Accepted default value ({(def ? "y" : "n")}) because the prompt timed out." + Environment.NewLine + Environment.NewLine); + return def; } + } - public async Task ExecuteProgressAsync(Func action) - { - var start = DateTime.UtcNow; - await AnsiConsole.Progress() - .AutoRefresh(true) - .AutoClear(false) - .HideCompleted(false) - .Columns(new ProgressColumn[] { - new SpinnerColumn(), - new TaskDescriptionColumn(), - new ProgressBarColumn(), - new PercentageColumn(), - new ElapsedTimeColumn(), - }) - .StartAsync(async ctx => await action(new Progress(logger, ctx))); - logger.Info($"[bold]Finished in {DateTime.UtcNow - start}.[/]"); - } + public void WriteLine(string text = "") + { + AnsiConsole.Markup(text + Environment.NewLine); + } - public async Task PromptYesNo(string prompt, bool? defaultValue = null, TimeSpan? timeout = null) - { - var def = defaultValue ?? defaultFactory.DefaultPromptValue; - var cts = new CancellationTokenSource(); - cts.CancelAfter(timeout ?? TimeSpan.FromSeconds(30)); + public void WriteTable(string tableName, IEnumerable> rows, bool hasHeaderRow = true) + { + // Create a table + var table = new Table(); + table.Title($"[bold underline]{tableName}[/]"); + table.Expand(); + table.LeftAligned(); - try { - // all of this nonsense in CancellableTextPrompt.cs is to work-around a bug in Spectre. - // Once the following issue is merged it can be removed. - // https://github.com/spectreconsole/spectre.console/pull/1439 - - AnsiConsole.WriteLine(); - var comparer = StringComparer.CurrentCultureIgnoreCase; - var textPrompt = "[underline bold orange3]QUESTION:[/]" + Environment.NewLine + prompt; - var clip = new CancellableTextPrompt(textPrompt, comparer); - clip.Choices.Add('y'); - clip.Choices.Add('n'); - clip.ShowChoices = true; - clip.ShowDefaultValue = true; - clip.DefaultValue = new DefaultPromptValue(def ? 'y' : 'n'); - var result = await clip.ShowAsync(AnsiConsole.Console, cts.Token).ConfigureAwait(false); - AnsiConsole.WriteLine(); - return comparer.Compare("y", result.ToString()) == 0; - } catch (OperationCanceledException) { - AnsiConsole.Write($" Accepted default value ({(def ? "y" : "n")}) because the prompt timed out." + Environment.NewLine + Environment.NewLine); - return def; + // Add some columns + if (hasHeaderRow) { + var headerRow = rows.First(); + rows = rows.Skip(1); + foreach (var header in headerRow) { + table.AddColumn(header); } - } - - public void WriteLine(string text = "") - { - AnsiConsole.Markup(text + Environment.NewLine); - } - - public void WriteTable(string tableName, IEnumerable> rows, bool hasHeaderRow = true) - { - // Create a table - var table = new Table(); - table.Title($"[bold underline]{tableName}[/]"); - table.Expand(); - table.LeftAligned(); - - // Add some columns - if (hasHeaderRow) { - var headerRow = rows.First(); - rows = rows.Skip(1); - foreach (var header in headerRow) { - table.AddColumn(header); - } - } else { - var numColumns = rows.First().Count(); - for (int i = 0; i < numColumns; i++) { - table.AddColumn($"Column {i}"); - } - table.HideHeaders(); + } else { + var numColumns = rows.First().Count(); + for (int i = 0; i < numColumns; i++) { + table.AddColumn($"Column {i}"); } - - // add rows - foreach (var row in rows) { - table.AddRow(row.ToArray()); - } - - // Render the table to the console - AnsiConsole.Write(table); + table.HideHeaders(); } - private class Progress : IFancyConsoleProgress - { - private readonly ILogger _logger; - private readonly ProgressContext _context; + // add rows + foreach (var row in rows) { + table.AddRow(row.ToArray()); + } - public Progress(ILogger logger, ProgressContext context) + // Render the table to the console + AnsiConsole.Write(table); + } + + private class Progress : IFancyConsoleProgress + { + private readonly ILogger _logger; + private readonly ProgressContext _context; + + public Progress(ILogger logger, ProgressContext context) + { + _logger = logger; + _context = context; + } + + public async Task RunTask(string name, Func, Task> fn) + { + _logger.Log(LogLevel.Debug, "Starting: " + name); + + var task = _context.AddTask($"[italic]{name}[/]"); + task.StartTask(); + + void progress(int p) { - _logger = logger; - _context = context; - } - - public async Task RunTask(string name, Func, Task> fn) - { - _logger.Log(LogLevel.Debug, "Starting: " + name); - - var task = _context.AddTask($"[italic]{name}[/]"); - task.StartTask(); - - void progress(int p) - { - if (p < 0) { - task.IsIndeterminate = true; - } else { - task.IsIndeterminate = false; - task.Value = Math.Min(100, p); - } + if (p < 0) { + task.IsIndeterminate = true; + } else { + task.IsIndeterminate = false; + task.Value = Math.Min(100, p); } - - await Task.Run(() => fn(progress)).ConfigureAwait(false); - task.IsIndeterminate = false; - task.StopTask(); - - _logger.Log(LogLevel.Debug, $"[bold]Complete: {name}[/]"); } + + await Task.Run(() => fn(progress)).ConfigureAwait(false); + task.IsIndeterminate = false; + task.StopTask(); + + _logger.Log(LogLevel.Debug, $"[bold]Complete: {name}[/]"); } } } diff --git a/test/LegacyTestApp/Program.cs b/test/LegacyTestApp/Program.cs index e13f679c..66175fe8 100644 --- a/test/LegacyTestApp/Program.cs +++ b/test/LegacyTestApp/Program.cs @@ -10,115 +10,114 @@ using Squirrel; [assembly: AssemblyMetadata("SquirrelAwareVersion", "1")] -namespace LegacyTestApp +namespace LegacyTestApp; + +internal class Program { - internal class Program + static int Main(string[] args) { - static int Main(string[] args) - { #if CLOWD - SquirrelAwareApp.HandleEvents( - onInitialInstall: (v, t) => debugFile("args.txt", String.Join(" ", args)), - onAppUpdate: (v, t) => debugFile("args.txt", String.Join(" ", args)), - onAppUninstall: (v, t) => debugFile("args.txt", String.Join(" ", args)), - onEveryRun: (v, t, f) => debugFile("args.txt", String.Join(" ", args)) - ); + SquirrelAwareApp.HandleEvents( + onInitialInstall: (v, t) => debugFile("args.txt", String.Join(" ", args)), + onAppUpdate: (v, t) => debugFile("args.txt", String.Join(" ", args)), + onAppUninstall: (v, t) => debugFile("args.txt", String.Join(" ", args)), + onEveryRun: (v, t, f) => debugFile("args.txt", String.Join(" ", args)) + ); #elif VELOPACK - VelopackApp.Build() - .WithAfterInstallFastCallback(v => debugFile("args.txt", String.Join(" ", args))) - .WithBeforeUpdateFastCallback(v => debugFile("args.txt", String.Join(" ", args))) - .WithBeforeUninstallFastCallback(v => debugFile("args.txt", String.Join(" ", args))) - .WithAfterUpdateFastCallback(v => debugFile("args.txt", String.Join(" ", args))) - .Run(); + VelopackApp.Build() + .WithAfterInstallFastCallback(v => debugFile("args.txt", String.Join(" ", args))) + .WithBeforeUpdateFastCallback(v => debugFile("args.txt", String.Join(" ", args))) + .WithBeforeUninstallFastCallback(v => debugFile("args.txt", String.Join(" ", args))) + .WithAfterUpdateFastCallback(v => debugFile("args.txt", String.Join(" ", args))) + .Run(); #else - SquirrelAwareApp.HandleEvents( - onInitialInstall: v => debugFile("args.txt", String.Join(" ", args)), - onAppUpdate: v => debugFile("args.txt", String.Join(" ", args)), - onAppUninstall: v => debugFile("args.txt", String.Join(" ", args)), - onFirstRun: () => debugFile("args.txt", String.Join(" ", args)) - ); + SquirrelAwareApp.HandleEvents( + onInitialInstall: v => debugFile("args.txt", String.Join(" ", args)), + onAppUpdate: v => debugFile("args.txt", String.Join(" ", args)), + onAppUninstall: v => debugFile("args.txt", String.Join(" ", args)), + onFirstRun: () => debugFile("args.txt", String.Join(" ", args)) + ); #endif - try { + try { #if !VELOPACK - SquirrelLogger.Register(); + SquirrelLogger.Register(); #endif - if (args.Length == 1 && args[0] == "version") { + if (args.Length == 1 && args[0] == "version") { #if VELOPACK - var um = new UpdateManager("n/a", logger: new SquirrelLogger()); - Console.WriteLine(um.CurrentVersion?.ToString() ?? "unknown_version"); + var um = new UpdateManager("n/a", logger: new SquirrelLogger()); + Console.WriteLine(um.CurrentVersion?.ToString() ?? "unknown_version"); #else - using var um = new UpdateManager(""); - Console.WriteLine(um.CurrentlyInstalledVersion()?.ToString() ?? "unknown_version"); + using var um = new UpdateManager(""); + Console.WriteLine(um.CurrentlyInstalledVersion()?.ToString() ?? "unknown_version"); +#endif + return 0; + } + + if (args.Length == 2) { + if (args[0] == "check") { +#if VELOPACK + var um = new UpdateManager(args[1]); + var info = um.CheckForUpdates(); + if (info == null || info.TargetFullRelease == null) { + Console.WriteLine("no updates"); + return 0; + } else { + Console.WriteLine("update: " + info.TargetFullRelease.Version); + return 0; + } +#else + using var um = new UpdateManager(args[1]); + var info = um.CheckForUpdate().GetAwaiter().GetResult(); + if (info == null || info.ReleasesToApply == null || info.FutureReleaseEntry == null || info.ReleasesToApply.Count == 0) { + Console.WriteLine("no updates"); + return 0; + } else { + Console.WriteLine("update: " + info.FutureReleaseEntry.Version); + return 0; + } +#endif + } + + if (args[0] == "download") { +#if VELOPACK + var um = new UpdateManager(args[1]); + var info = um.CheckForUpdates(); + if (info == null) return -1; + um.DownloadUpdates(info); + return 0; +#else + using var um = new UpdateManager(args[1]); + var entry = um.UpdateApp().GetAwaiter().GetResult(); + return entry == null ? -1 : 0; +#endif + } + + if (args[0] == "apply") { +#if VELOPACK + var um = new UpdateManager(args[1]); + um.ApplyUpdatesAndRestart(); +#else + UpdateManager.RestartApp(); #endif return 0; } - - if (args.Length == 2) { - if (args[0] == "check") { -#if VELOPACK - var um = new UpdateManager(args[1]); - var info = um.CheckForUpdates(); - if (info == null || info.TargetFullRelease == null) { - Console.WriteLine("no updates"); - return 0; - } else { - Console.WriteLine("update: " + info.TargetFullRelease.Version); - return 0; - } -#else - using var um = new UpdateManager(args[1]); - var info = um.CheckForUpdate().GetAwaiter().GetResult(); - if (info == null || info.ReleasesToApply == null || info.FutureReleaseEntry == null || info.ReleasesToApply.Count == 0) { - Console.WriteLine("no updates"); - return 0; - } else { - Console.WriteLine("update: " + info.FutureReleaseEntry.Version); - return 0; - } -#endif - } - - if (args[0] == "download") { -#if VELOPACK - var um = new UpdateManager(args[1]); - var info = um.CheckForUpdates(); - if (info == null) return -1; - um.DownloadUpdates(info); - return 0; -#else - using var um = new UpdateManager(args[1]); - var entry = um.UpdateApp().GetAwaiter().GetResult(); - return entry == null ? -1 : 0; -#endif - } - - if (args[0] == "apply") { -#if VELOPACK - var um = new UpdateManager(args[1]); - um.ApplyUpdatesAndRestart(); -#else - UpdateManager.RestartApp(); -#endif - return 0; - } - } - - } catch (Exception ex) { - Console.WriteLine("exception: " + ex.ToString()); - if (Debugger.IsAttached) throw; - return -1; } - Console.WriteLine("Unhandled args: " + String.Join(", ", args)); + } catch (Exception ex) { + Console.WriteLine("exception: " + ex.ToString()); + if (Debugger.IsAttached) throw; return -1; } - static void debugFile(string name, string message) - { - var path = Path.Combine(AppContext.BaseDirectory, "..", name); - File.AppendAllText(path, message + Environment.NewLine); - } + Console.WriteLine("Unhandled args: " + String.Join(", ", args)); + return -1; + } + + static void debugFile(string name, string message) + { + var path = Path.Combine(AppContext.BaseDirectory, "..", name); + File.AppendAllText(path, message + Environment.NewLine); } } diff --git a/test/LegacyTestApp/SquirrelLogger.cs b/test/LegacyTestApp/SquirrelLogger.cs index 966097e7..5f9d3b46 100644 --- a/test/LegacyTestApp/SquirrelLogger.cs +++ b/test/LegacyTestApp/SquirrelLogger.cs @@ -1,44 +1,43 @@ using System; -namespace LegacyTestApp -{ +namespace LegacyTestApp; + #if VELOPACK - using Microsoft.Extensions.Logging; - class SquirrelLogger : ILogger +using Microsoft.Extensions.Logging; +class SquirrelLogger : ILogger +{ + public IDisposable BeginScope(TState state) where TState : notnull { - public IDisposable BeginScope(TState state) where TState : notnull - { - return null; - } - - public bool IsEnabled(LogLevel logLevel) - { - return true; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - Console.WriteLine(formatter(state, exception)); - } + return null; } -#else - class SquirrelLogger : Squirrel.SimpleSplat.ILogger + + public bool IsEnabled(LogLevel logLevel) { - protected SquirrelLogger() - { - } - - public Squirrel.SimpleSplat.LogLevel Level { get; set; } - - public static void Register() - { - Squirrel.SimpleSplat.SquirrelLocator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Squirrel.SimpleSplat.ILogger)); - } - - public void Write(string message, Squirrel.SimpleSplat.LogLevel logLevel) - { - Console.WriteLine(message); - } + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + Console.WriteLine(formatter(state, exception)); } -#endif } +#else +class SquirrelLogger : Squirrel.SimpleSplat.ILogger +{ + protected SquirrelLogger() + { + } + + public Squirrel.SimpleSplat.LogLevel Level { get; set; } + + public static void Register() + { + Squirrel.SimpleSplat.SquirrelLocator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Squirrel.SimpleSplat.ILogger)); + } + + public void Write(string message, Squirrel.SimpleSplat.LogLevel logLevel) + { + Console.WriteLine(message); + } +} +#endif diff --git a/test/Velopack.Packaging.Tests/DotnetUtilTests.cs b/test/Velopack.Packaging.Tests/DotnetUtilTests.cs index edc17980..0a8870a9 100644 --- a/test/Velopack.Packaging.Tests/DotnetUtilTests.cs +++ b/test/Velopack.Packaging.Tests/DotnetUtilTests.cs @@ -7,102 +7,101 @@ using Velopack.Packaging.Exceptions; using Velopack.Packaging.Windows; using Velopack.Packaging.Windows.Commands; -namespace Velopack.Packaging.Tests +namespace Velopack.Packaging.Tests; + +public class DotnetUtilTests { - public class DotnetUtilTests + private readonly ITestOutputHelper _output; + + public DotnetUtilTests(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; + _output = output; + } - public DotnetUtilTests(ITestOutputHelper output) - { - _output = output; - } + [SkippableFact] + public void NonDotnetBinaryPasses() + { + Skip.IfNot(VelopackRuntimeInfo.IsWindows); + using var logger = _output.BuildLoggerFor(); + Assert.Null(DotnetUtil.VerifyVelopackApp(PathHelper.GetRustAsset("testapp.exe"), logger)); + } - [SkippableFact] - public void NonDotnetBinaryPasses() - { - Skip.IfNot(VelopackRuntimeInfo.IsWindows); - using var logger = _output.BuildLoggerFor(); - Assert.Null(DotnetUtil.VerifyVelopackApp(PathHelper.GetRustAsset("testapp.exe"), logger)); - } + [SkippableFact] + public void PublishSingleFilePasses() + { + Skip.IfNot(VelopackRuntimeInfo.IsWindows); + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var dir); + var sample = PathHelper.GetAvaloniaSample(); + Exe.InvokeAndThrowIfNonZero( + "dotnet", + new string[] { "publish", "--no-self-contained", "-r", "win-x64", "-o", dir, + "-p:UseLocalVelopack=true", "-p:PublishSingleFile=true" }, + sample); - [SkippableFact] - public void PublishSingleFilePasses() - { - Skip.IfNot(VelopackRuntimeInfo.IsWindows); - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var dir); - var sample = PathHelper.GetAvaloniaSample(); - Exe.InvokeAndThrowIfNonZero( - "dotnet", - new string[] { "publish", "--no-self-contained", "-r", "win-x64", "-o", dir, - "-p:UseLocalVelopack=true", "-p:PublishSingleFile=true" }, - sample); + var path = Path.Combine(dir, "AvaloniaCrossPlat.exe"); + Assert.Equal(VelopackRuntimeInfo.VelopackProductVersion, DotnetUtil.VerifyVelopackApp(path, logger)); - var path = Path.Combine(dir, "AvaloniaCrossPlat.exe"); - Assert.Equal(VelopackRuntimeInfo.VelopackProductVersion, DotnetUtil.VerifyVelopackApp(path, logger)); + var newPath = Path.Combine(dir, "AvaloniaCrossPlat-asd2.exe"); + File.Move(path, newPath); + Assert.Equal(VelopackRuntimeInfo.VelopackProductVersion, DotnetUtil.VerifyVelopackApp(newPath, logger)); + } - var newPath = Path.Combine(dir, "AvaloniaCrossPlat-asd2.exe"); - File.Move(path, newPath); - Assert.Equal(VelopackRuntimeInfo.VelopackProductVersion, DotnetUtil.VerifyVelopackApp(newPath, logger)); - } + [SkippableFact] + public void PublishDotnet6Passes() + { + Skip.IfNot(VelopackRuntimeInfo.IsWindows); + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var dir); + var sample = PathHelper.GetAvaloniaSample(); + Exe.InvokeAndThrowIfNonZero( + "dotnet", + new string[] { "publish", "--no-self-contained", "-r", "win-x64", "-o", dir, + "-p:UseLocalVelopack=true" }, + sample); - [SkippableFact] - public void PublishDotnet6Passes() - { - Skip.IfNot(VelopackRuntimeInfo.IsWindows); - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var dir); - var sample = PathHelper.GetAvaloniaSample(); - Exe.InvokeAndThrowIfNonZero( - "dotnet", - new string[] { "publish", "--no-self-contained", "-r", "win-x64", "-o", dir, - "-p:UseLocalVelopack=true" }, - sample); + var path = Path.Combine(dir, "AvaloniaCrossPlat.exe"); + Assert.Equal(VelopackRuntimeInfo.VelopackProductVersion, DotnetUtil.VerifyVelopackApp(path, logger)); - var path = Path.Combine(dir, "AvaloniaCrossPlat.exe"); - Assert.Equal(VelopackRuntimeInfo.VelopackProductVersion, DotnetUtil.VerifyVelopackApp(path, logger)); + var newPath = Path.Combine(dir, "AvaloniaCrossPlat-asd2.exe"); + File.Move(path, newPath); + Assert.Equal(VelopackRuntimeInfo.VelopackProductVersion, DotnetUtil.VerifyVelopackApp(newPath, logger)); + } - var newPath = Path.Combine(dir, "AvaloniaCrossPlat-asd2.exe"); - File.Move(path, newPath); - Assert.Equal(VelopackRuntimeInfo.VelopackProductVersion, DotnetUtil.VerifyVelopackApp(newPath, logger)); - } + [SkippableFact] + public void PublishNet48Passes() + { + Skip.IfNot(VelopackRuntimeInfo.IsWindows); + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var dir); + var sample = PathHelper.GetWpfSample(); + Exe.InvokeAndThrowIfNonZero( + "dotnet", + new string[] { "publish", "-o", dir }, + sample); - [SkippableFact] - public void PublishNet48Passes() - { - Skip.IfNot(VelopackRuntimeInfo.IsWindows); - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var dir); - var sample = PathHelper.GetWpfSample(); - Exe.InvokeAndThrowIfNonZero( - "dotnet", - new string[] { "publish", "-o", dir }, - sample); + var path = Path.Combine(dir, "VeloWpfSample.exe"); + Assert.NotNull(DotnetUtil.VerifyVelopackApp(path, logger)); - var path = Path.Combine(dir, "VeloWpfSample.exe"); - Assert.NotNull(DotnetUtil.VerifyVelopackApp(path, logger)); + var newPath = Path.Combine(dir, "VeloWpfSample-asd2.exe"); + File.Move(path, newPath); + Assert.NotNull(DotnetUtil.VerifyVelopackApp(newPath, logger)); + } - var newPath = Path.Combine(dir, "VeloWpfSample-asd2.exe"); - File.Move(path, newPath); - Assert.NotNull(DotnetUtil.VerifyVelopackApp(newPath, logger)); - } + [SkippableFact] + public void UnawareDotnetAppFails() + { + Skip.IfNot(VelopackRuntimeInfo.IsWindows); + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var dir); + var sample = PathHelper.GetTestRootPath("TestApp"); + Exe.InvokeAndThrowIfNonZero( + "dotnet", + new string[] { "publish", "--no-self-contained", "-r", "win-x64", "-o", dir, + "-p:NoVelopackApp=true" }, + sample); - [SkippableFact] - public void UnawareDotnetAppFails() - { - Skip.IfNot(VelopackRuntimeInfo.IsWindows); - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var dir); - var sample = PathHelper.GetTestRootPath("TestApp"); - Exe.InvokeAndThrowIfNonZero( - "dotnet", - new string[] { "publish", "--no-self-contained", "-r", "win-x64", "-o", dir, - "-p:NoVelopackApp=true" }, - sample); - - var path = Path.Combine(dir, "TestApp.exe"); - Assert.Throws(() => DotnetUtil.VerifyVelopackApp(path, logger)); - } + var path = Path.Combine(dir, "TestApp.exe"); + Assert.Throws(() => DotnetUtil.VerifyVelopackApp(path, logger)); } } diff --git a/test/Velopack.Packaging.Tests/GithubDeploymentTests.cs b/test/Velopack.Packaging.Tests/GithubDeploymentTests.cs index ec8c097d..0469058b 100644 --- a/test/Velopack.Packaging.Tests/GithubDeploymentTests.cs +++ b/test/Velopack.Packaging.Tests/GithubDeploymentTests.cs @@ -6,232 +6,231 @@ using Velopack.Sources; using Octokit; using Velopack.Packaging.Exceptions; -namespace Velopack.Packaging.Tests +namespace Velopack.Packaging.Tests; + +public class GithubDeploymentTests { - public class GithubDeploymentTests + public readonly static string GITHUB_TOKEN = Environment.GetEnvironmentVariable("VELOPACK_GITHUB_TEST_TOKEN"); + public readonly static string GITHUB_REPOURL = "https://github.com/caesay/VelopackGithubUpdateTest"; + + private readonly ITestOutputHelper _output; + + public GithubDeploymentTests(ITestOutputHelper output) { - public readonly static string GITHUB_TOKEN = Environment.GetEnvironmentVariable("VELOPACK_GITHUB_TEST_TOKEN"); - public readonly static string GITHUB_REPOURL = "https://github.com/caesay/VelopackGithubUpdateTest"; + _output = output; + } - private readonly ITestOutputHelper _output; + [SkippableFact] + public void WillRefuseToUploadMultipleWithoutMergeArg() + { + Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set."); + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var releaseDir); + using var _2 = Utility.GetTempDirectory(out var releaseDir2); + using var ghvar = GitHubReleaseTest.Create("nomerge", logger); + var id = "GithubUpdateTest"; + TestApp.PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir, logger); - public GithubDeploymentTests(ITestOutputHelper output) - { - _output = output; - } + var gh = new GitHubRepository(logger); + var options = new GitHubUploadOptions { + ReleaseName = ghvar.ReleaseName, + ReleaseDir = new DirectoryInfo(releaseDir), + RepoUrl = GITHUB_REPOURL, + Token = GITHUB_TOKEN, + Prerelease = false, + Publish = true, + }; - [SkippableFact] - public void WillRefuseToUploadMultipleWithoutMergeArg() - { - Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set."); - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var releaseDir); - using var _2 = Utility.GetTempDirectory(out var releaseDir2); - using var ghvar = GitHubReleaseTest.Create("nomerge", logger); - var id = "GithubUpdateTest"; - TestApp.PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir, logger); + gh.UploadMissingAssetsAsync(options).GetAwaiterResult(); - var gh = new GitHubRepository(logger); - var options = new GitHubUploadOptions { - ReleaseName = ghvar.ReleaseName, - ReleaseDir = new DirectoryInfo(releaseDir), - RepoUrl = GITHUB_REPOURL, - Token = GITHUB_TOKEN, - Prerelease = false, - Publish = true, - }; + TestApp.PackTestApp(id, $"0.0.2-{ghvar.UniqueSuffix}", "t1", releaseDir2, logger); + options.ReleaseDir = new DirectoryInfo(releaseDir2); - gh.UploadMissingAssetsAsync(options).GetAwaiterResult(); + Assert.ThrowsAny(() => gh.UploadMissingAssetsAsync(options).GetAwaiterResult()); + } - TestApp.PackTestApp(id, $"0.0.2-{ghvar.UniqueSuffix}", "t1", releaseDir2, logger); - options.ReleaseDir = new DirectoryInfo(releaseDir2); + [SkippableFact] + public void WillNotMergeMixmatchedTag() + { + Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set."); + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var releaseDir); + using var _2 = Utility.GetTempDirectory(out var releaseDir2); + using var ghvar = GitHubReleaseTest.Create("mixmatched", logger); + var id = "GithubUpdateTest"; + TestApp.PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir, logger); - Assert.ThrowsAny(() => gh.UploadMissingAssetsAsync(options).GetAwaiterResult()); - } + var gh = new GitHubRepository(logger); + var options = new GitHubUploadOptions { + ReleaseName = ghvar.ReleaseName, + ReleaseDir = new DirectoryInfo(releaseDir), + RepoUrl = GITHUB_REPOURL, + Token = GITHUB_TOKEN, + Prerelease = false, + Publish = true, + Merge = true, + }; - [SkippableFact] - public void WillNotMergeMixmatchedTag() - { - Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set."); - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var releaseDir); - using var _2 = Utility.GetTempDirectory(out var releaseDir2); - using var ghvar = GitHubReleaseTest.Create("mixmatched", logger); - var id = "GithubUpdateTest"; - TestApp.PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir, logger); + gh.UploadMissingAssetsAsync(options).GetAwaiterResult(); - var gh = new GitHubRepository(logger); - var options = new GitHubUploadOptions { - ReleaseName = ghvar.ReleaseName, - ReleaseDir = new DirectoryInfo(releaseDir), - RepoUrl = GITHUB_REPOURL, - Token = GITHUB_TOKEN, - Prerelease = false, - Publish = true, - Merge = true, - }; + TestApp.PackTestApp(id, $"0.0.2-{ghvar.UniqueSuffix}", "t1", releaseDir2, logger); + options.ReleaseDir = new DirectoryInfo(releaseDir2); - gh.UploadMissingAssetsAsync(options).GetAwaiterResult(); + Assert.ThrowsAny(() => gh.UploadMissingAssetsAsync(options).GetAwaiterResult()); + } - TestApp.PackTestApp(id, $"0.0.2-{ghvar.UniqueSuffix}", "t1", releaseDir2, logger); - options.ReleaseDir = new DirectoryInfo(releaseDir2); + [SkippableFact] + public void WillMergeGithubReleases() + { + Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set."); + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var releaseDir); + using var _2 = Utility.GetTempDirectory(out var releaseDir2); + using var ghvar = GitHubReleaseTest.Create("yesmerge", logger); + var id = "GithubUpdateTest"; + TestApp.PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir, logger); - Assert.ThrowsAny(() => gh.UploadMissingAssetsAsync(options).GetAwaiterResult()); - } + var gh = new GitHubRepository(logger); + var options = new GitHubUploadOptions { + ReleaseName = ghvar.ReleaseName, + ReleaseDir = new DirectoryInfo(releaseDir), + RepoUrl = GITHUB_REPOURL, + Token = GITHUB_TOKEN, + TagName = $"0.0.1-{ghvar.UniqueSuffix}", + Prerelease = false, + Publish = true, + Merge = true, + }; - [SkippableFact] - public void WillMergeGithubReleases() - { - Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set."); - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var releaseDir); - using var _2 = Utility.GetTempDirectory(out var releaseDir2); - using var ghvar = GitHubReleaseTest.Create("yesmerge", logger); - var id = "GithubUpdateTest"; - TestApp.PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir, logger); + gh.UploadMissingAssetsAsync(options).GetAwaiterResult(); - var gh = new GitHubRepository(logger); - var options = new GitHubUploadOptions { - ReleaseName = ghvar.ReleaseName, - ReleaseDir = new DirectoryInfo(releaseDir), - RepoUrl = GITHUB_REPOURL, - Token = GITHUB_TOKEN, - TagName = $"0.0.1-{ghvar.UniqueSuffix}", - Prerelease = false, - Publish = true, - Merge = true, - }; + TestApp.PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir2, logger, channel: "experimental"); + options.ReleaseDir = new DirectoryInfo(releaseDir2); + options.Channel = "experimental"; - gh.UploadMissingAssetsAsync(options).GetAwaiterResult(); + gh.UploadMissingAssetsAsync(options).GetAwaiterResult(); + } - TestApp.PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir2, logger, channel: "experimental"); - options.ReleaseDir = new DirectoryInfo(releaseDir2); - options.Channel = "experimental"; + [SkippableFact] + public void CanDeployAndUpdateFromGithub() + { + Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set."); + using var logger = _output.BuildLoggerFor(); + var id = "GithubUpdateTest"; + using var _1 = Utility.GetTempDirectory(out var releaseDir); + var (repoOwner, repoName) = GitHubRepository.GetOwnerAndRepo(GITHUB_REPOURL); + using var ghvar = GitHubReleaseTest.Create("integration", logger); + var releaseName = ghvar.ReleaseName; + var uniqueSuffix = ghvar.UniqueSuffix; + var client = ghvar.Client; - gh.UploadMissingAssetsAsync(options).GetAwaiterResult(); - } - - [SkippableFact] - public void CanDeployAndUpdateFromGithub() - { - Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set."); - using var logger = _output.BuildLoggerFor(); - var id = "GithubUpdateTest"; - using var _1 = Utility.GetTempDirectory(out var releaseDir); - var (repoOwner, repoName) = GitHubRepository.GetOwnerAndRepo(GITHUB_REPOURL); - using var ghvar = GitHubReleaseTest.Create("integration", logger); - var releaseName = ghvar.ReleaseName; - var uniqueSuffix = ghvar.UniqueSuffix; - var client = ghvar.Client; - - // create releases - var notesPath = Path.Combine(releaseDir, "NOTES"); - var notesContent = $""" + // create releases + var notesPath = Path.Combine(releaseDir, "NOTES"); + var notesContent = $""" # Release {releaseName} This is just a _test_! """; - File.WriteAllText(notesPath, notesContent); + File.WriteAllText(notesPath, notesContent); - if (String.IsNullOrEmpty(GITHUB_TOKEN)) - throw new Exception("VELOPACK_GITHUB_TEST_TOKEN is not set."); + if (String.IsNullOrEmpty(GITHUB_TOKEN)) + throw new Exception("VELOPACK_GITHUB_TEST_TOKEN is not set."); - var newVer = $"{VelopackRuntimeInfo.VelopackNugetVersion}"; - TestApp.PackTestApp(id, $"0.0.1", "t1", releaseDir, logger, notesPath, channel: uniqueSuffix); - TestApp.PackTestApp(id, newVer, "t2", releaseDir, logger, notesPath, channel: uniqueSuffix); + var newVer = $"{VelopackRuntimeInfo.VelopackNugetVersion}"; + TestApp.PackTestApp(id, $"0.0.1", "t1", releaseDir, logger, notesPath, channel: uniqueSuffix); + TestApp.PackTestApp(id, newVer, "t2", releaseDir, logger, notesPath, channel: uniqueSuffix); - // deploy - var gh = new GitHubRepository(logger); - var options = new GitHubUploadOptions { - ReleaseName = releaseName, - ReleaseDir = new DirectoryInfo(releaseDir), - RepoUrl = GITHUB_REPOURL, - Token = GITHUB_TOKEN, - Prerelease = false, - Publish = true, - Channel = uniqueSuffix, - }; - gh.UploadMissingAssetsAsync(options).GetAwaiterResult(); + // deploy + var gh = new GitHubRepository(logger); + var options = new GitHubUploadOptions { + ReleaseName = releaseName, + ReleaseDir = new DirectoryInfo(releaseDir), + RepoUrl = GITHUB_REPOURL, + Token = GITHUB_TOKEN, + Prerelease = false, + Publish = true, + Channel = uniqueSuffix, + }; + gh.UploadMissingAssetsAsync(options).GetAwaiterResult(); - // check - var newRelease = client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().Single(s => s.Name == releaseName); - Assert.False(newRelease.Draft); - Assert.Equal(notesContent.Trim().ReplaceLineEndings("\n"), newRelease.Body.Trim()); + // check + var newRelease = client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().Single(s => s.Name == releaseName); + Assert.False(newRelease.Draft); + Assert.Equal(notesContent.Trim().ReplaceLineEndings("\n"), newRelease.Body.Trim()); - // update - var source = new GithubSource(GITHUB_REPOURL, GITHUB_TOKEN, false); - var releases = source.GetReleaseFeed(channel: uniqueSuffix, logger: logger).GetAwaiterResult(); + // update + var source = new GithubSource(GITHUB_REPOURL, GITHUB_TOKEN, false); + var releases = source.GetReleaseFeed(channel: uniqueSuffix, logger: logger).GetAwaiterResult(); - var ghrel = releases.Assets.Select(r => (GithubSource.GitBaseAsset) r).ToArray(); - foreach (var g in ghrel) { - logger.Info($"Found asset: ({g.Release.Name}) {g.FileName}"); - } - - var assetsInThisRelease = ghrel.Where(r => r.Release.Name == releaseName).ToArray(); - - Assert.Equal(2, assetsInThisRelease.Length); - foreach (var r in assetsInThisRelease) { - Assert.Equal(releaseName, r.Release.Name); - Assert.Equal(id, r.PackageId); - Assert.Equal(newVer, r.Version.ToNormalizedString()); - } - - using var _2 = Utility.GetTempDirectory(out var releaseDirNew); - gh.DownloadLatestFullPackageAsync(new GitHubDownloadOptions { - Token = GITHUB_TOKEN, - RepoUrl = GITHUB_REPOURL, - ReleaseDir = new DirectoryInfo(releaseDirNew), - Channel = uniqueSuffix, - }).GetAwaiterResult(); - - var filename = $"{id}-{newVer}-{uniqueSuffix}-full.nupkg"; - Assert.True(File.Exists(Path.Combine(releaseDirNew, filename))); + var ghrel = releases.Assets.Select(r => (GithubSource.GitBaseAsset) r).ToArray(); + foreach (var g in ghrel) { + logger.Info($"Found asset: ({g.Release.Name}) {g.FileName}"); } - private class GitHubReleaseTest : IDisposable - { - public string ReleaseName { get; } - public string UniqueSuffix { get; } - public GitHubClient Client { get; } - public ILogger Logger { get; } + var assetsInThisRelease = ghrel.Where(r => r.Release.Name == releaseName).ToArray(); - public GitHubReleaseTest(string releaseName, string uniqueSuffix, GitHubClient client, ILogger logger) - { - ReleaseName = releaseName; - UniqueSuffix = uniqueSuffix; - Client = client; - Logger = logger; - } - - public static GitHubReleaseTest Create(string method, ILogger logger) - { - var ci = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")); - var uniqueSuffix = (ci ? "ci-" : "local-") + VelopackRuntimeInfo.SystemOs.GetOsShortName(); - var releaseName = $"{VelopackRuntimeInfo.VelopackNugetVersion}-{uniqueSuffix}-{method}"; - var (repoOwner, repoName) = GitHubRepository.GetOwnerAndRepo(GITHUB_REPOURL); - - // delete release if already exists - var client = new GitHubClient(new ProductHeaderValue("Velopack")) { - Credentials = new Credentials(GITHUB_TOKEN) - }; - var existingRelease = client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().SingleOrDefault(s => s.Name == releaseName); - if (existingRelease != null) { - client.Repository.Release.Delete(repoOwner, repoName, existingRelease.Id).GetAwaiterResult(); - logger.Info("Deleted existing release: " + releaseName); - } - return new GitHubReleaseTest(releaseName, uniqueSuffix, client, logger); - } - - public void Dispose() - { - var (repoOwner, repoName) = GitHubRepository.GetOwnerAndRepo(GITHUB_REPOURL); - var finalRelease = Client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().SingleOrDefault(s => s.Name == ReleaseName); - if (finalRelease != null) { - Client.Repository.Release.Delete(repoOwner, repoName, finalRelease.Id).GetAwaiterResult(); - Logger.Info($"Deleted final release '{ReleaseName}'"); - } - } + Assert.Equal(2, assetsInThisRelease.Length); + foreach (var r in assetsInThisRelease) { + Assert.Equal(releaseName, r.Release.Name); + Assert.Equal(id, r.PackageId); + Assert.Equal(newVer, r.Version.ToNormalizedString()); } + using var _2 = Utility.GetTempDirectory(out var releaseDirNew); + gh.DownloadLatestFullPackageAsync(new GitHubDownloadOptions { + Token = GITHUB_TOKEN, + RepoUrl = GITHUB_REPOURL, + ReleaseDir = new DirectoryInfo(releaseDirNew), + Channel = uniqueSuffix, + }).GetAwaiterResult(); + var filename = $"{id}-{newVer}-{uniqueSuffix}-full.nupkg"; + Assert.True(File.Exists(Path.Combine(releaseDirNew, filename))); } + + private class GitHubReleaseTest : IDisposable + { + public string ReleaseName { get; } + public string UniqueSuffix { get; } + public GitHubClient Client { get; } + public ILogger Logger { get; } + + public GitHubReleaseTest(string releaseName, string uniqueSuffix, GitHubClient client, ILogger logger) + { + ReleaseName = releaseName; + UniqueSuffix = uniqueSuffix; + Client = client; + Logger = logger; + } + + public static GitHubReleaseTest Create(string method, ILogger logger) + { + var ci = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")); + var uniqueSuffix = (ci ? "ci-" : "local-") + VelopackRuntimeInfo.SystemOs.GetOsShortName(); + var releaseName = $"{VelopackRuntimeInfo.VelopackNugetVersion}-{uniqueSuffix}-{method}"; + var (repoOwner, repoName) = GitHubRepository.GetOwnerAndRepo(GITHUB_REPOURL); + + // delete release if already exists + var client = new GitHubClient(new ProductHeaderValue("Velopack")) { + Credentials = new Credentials(GITHUB_TOKEN) + }; + var existingRelease = client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().SingleOrDefault(s => s.Name == releaseName); + if (existingRelease != null) { + client.Repository.Release.Delete(repoOwner, repoName, existingRelease.Id).GetAwaiterResult(); + logger.Info("Deleted existing release: " + releaseName); + } + return new GitHubReleaseTest(releaseName, uniqueSuffix, client, logger); + } + + public void Dispose() + { + var (repoOwner, repoName) = GitHubRepository.GetOwnerAndRepo(GITHUB_REPOURL); + var finalRelease = Client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().SingleOrDefault(s => s.Name == ReleaseName); + if (finalRelease != null) { + Client.Repository.Release.Delete(repoOwner, repoName, finalRelease.Id).GetAwaiterResult(); + Logger.Info($"Deleted final release '{ReleaseName}'"); + } + } + } + + } diff --git a/test/Velopack.Packaging.Tests/Init.cs b/test/Velopack.Packaging.Tests/Init.cs index 1d09d448..e6c1235a 100644 --- a/test/Velopack.Packaging.Tests/Init.cs +++ b/test/Velopack.Packaging.Tests/Init.cs @@ -2,15 +2,14 @@ [assembly: TestFramework("Velopack.Packaging.Tests.TestsInit", "Velopack.Packaging.Tests")] -namespace Velopack.Packaging.Tests +namespace Velopack.Packaging.Tests; + +public class TestsInit : XunitTestFramework { - public class TestsInit : XunitTestFramework + public TestsInit(IMessageSink messageSink) + : base(messageSink) { - public TestsInit(IMessageSink messageSink) - : base(messageSink) - { - HelperFile.AddSearchPath(PathHelper.GetRustBuildOutputDir()); - HelperFile.AddSearchPath(PathHelper.GetVendorLibDir()); - } + HelperFile.AddSearchPath(PathHelper.GetRustBuildOutputDir()); + HelperFile.AddSearchPath(PathHelper.GetVendorLibDir()); } } diff --git a/test/Velopack.Packaging.Tests/S3DeploymentTests.cs b/test/Velopack.Packaging.Tests/S3DeploymentTests.cs index 3ff50643..02b2fbcb 100644 --- a/test/Velopack.Packaging.Tests/S3DeploymentTests.cs +++ b/test/Velopack.Packaging.Tests/S3DeploymentTests.cs @@ -8,80 +8,79 @@ using Velopack.Deployment; using Velopack.Packaging.Exceptions; using Velopack.Sources; -namespace Velopack.Packaging.Tests +namespace Velopack.Packaging.Tests; + +public class S3DeploymentTests { - public class S3DeploymentTests + public readonly static string B2_KEYID = "0035016844a4188000000000a"; + public readonly static string B2_SECRET = Environment.GetEnvironmentVariable("VELOPACK_B2_TEST_TOKEN"); + public readonly static string B2_BUCKET = "velopack-testing"; + public readonly static string B2_ENDPOINT = "s3.eu-central-003.backblazeb2.com"; + + private readonly ITestOutputHelper _output; + + public S3DeploymentTests(ITestOutputHelper output) { - public readonly static string B2_KEYID = "0035016844a4188000000000a"; - public readonly static string B2_SECRET = Environment.GetEnvironmentVariable("VELOPACK_B2_TEST_TOKEN"); - public readonly static string B2_BUCKET = "velopack-testing"; - public readonly static string B2_ENDPOINT = "s3.eu-central-003.backblazeb2.com"; + _output = output; + } - private readonly ITestOutputHelper _output; + [SkippableFact] + public void CanDeployToBackBlazeB2() + { + Skip.If(String.IsNullOrWhiteSpace(B2_SECRET), "VELOPACK_B2_TEST_TOKEN is not set."); + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var releaseDir); - public S3DeploymentTests(ITestOutputHelper output) - { - _output = output; - } + string channel = String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CI")) + ? VelopackRuntimeInfo.SystemOs.GetOsShortName() + : "ci-" + VelopackRuntimeInfo.SystemOs.GetOsShortName(); - [SkippableFact] - public void CanDeployToBackBlazeB2() - { - Skip.If(String.IsNullOrWhiteSpace(B2_SECRET), "VELOPACK_B2_TEST_TOKEN is not set."); - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var releaseDir); - - string channel = String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CI")) - ? VelopackRuntimeInfo.SystemOs.GetOsShortName() - : "ci-" + VelopackRuntimeInfo.SystemOs.GetOsShortName(); - - // get latest version, and increment patch by one - var updateUrl = $"https://{B2_BUCKET}.{B2_ENDPOINT}/"; - var source = new SimpleWebSource(updateUrl); - VelopackAssetFeed feed = new VelopackAssetFeed(); - try { - feed = source.GetReleaseFeed(logger, channel).GetAwaiterResult(); - } catch (Exception ex) { - logger.Warn(ex, "Failed to fetch release feed."); - } - var latest = feed.Assets.Where(a => a.Version != null && a.Type == VelopackAssetType.Full) - .OrderByDescending(a => a.Version) - .FirstOrDefault(); - var newVer = latest != null ? new SemanticVersion(1, 0, latest.Version.Patch + 1) : new SemanticVersion(1, 0, 0); - - // create repo - var repo = new S3Repository(logger); - var options = new S3UploadOptions { - ReleaseDir = new DirectoryInfo(releaseDir), - Bucket = B2_BUCKET, - Channel = channel, - Endpoint = "https://" + B2_ENDPOINT, - KeyId = B2_KEYID, - Secret = B2_SECRET, - KeepMaxReleases = 4, - }; - - // download latest version and create delta - repo.DownloadLatestFullPackageAsync(options).GetAwaiterResult(); - var id = "B2TestApp"; - TestApp.PackTestApp(id, newVer.ToFullString(), $"b2-{DateTime.UtcNow.ToLongDateString()}", releaseDir, logger, channel: channel); - if (latest != null) { - // check delta was created - Assert.True(Directory.EnumerateFiles(releaseDir, "*-delta.nupkg").Any(), "No delta package was created."); - } - - // upload new files - repo.UploadMissingAssetsAsync(options).GetAwaiterResult(); - - // verify that new version has been uploaded + // get latest version, and increment patch by one + var updateUrl = $"https://{B2_BUCKET}.{B2_ENDPOINT}/"; + var source = new SimpleWebSource(updateUrl); + VelopackAssetFeed feed = new VelopackAssetFeed(); + try { feed = source.GetReleaseFeed(logger, channel).GetAwaiterResult(); - latest = feed.Assets.Where(a => a.Version != null && a.Type == VelopackAssetType.Full) - .OrderByDescending(a => a.Version) - .FirstOrDefault(); - - Assert.True(latest != null, "No latest version found."); - Assert.Equal(newVer, latest.Version); - Assert.True(feed.Assets.Count(x => x.Type == VelopackAssetType.Full) <= options.KeepMaxReleases, "Too many releases were kept."); + } catch (Exception ex) { + logger.Warn(ex, "Failed to fetch release feed."); } + var latest = feed.Assets.Where(a => a.Version != null && a.Type == VelopackAssetType.Full) + .OrderByDescending(a => a.Version) + .FirstOrDefault(); + var newVer = latest != null ? new SemanticVersion(1, 0, latest.Version.Patch + 1) : new SemanticVersion(1, 0, 0); + + // create repo + var repo = new S3Repository(logger); + var options = new S3UploadOptions { + ReleaseDir = new DirectoryInfo(releaseDir), + Bucket = B2_BUCKET, + Channel = channel, + Endpoint = "https://" + B2_ENDPOINT, + KeyId = B2_KEYID, + Secret = B2_SECRET, + KeepMaxReleases = 4, + }; + + // download latest version and create delta + repo.DownloadLatestFullPackageAsync(options).GetAwaiterResult(); + var id = "B2TestApp"; + TestApp.PackTestApp(id, newVer.ToFullString(), $"b2-{DateTime.UtcNow.ToLongDateString()}", releaseDir, logger, channel: channel); + if (latest != null) { + // check delta was created + Assert.True(Directory.EnumerateFiles(releaseDir, "*-delta.nupkg").Any(), "No delta package was created."); + } + + // upload new files + repo.UploadMissingAssetsAsync(options).GetAwaiterResult(); + + // verify that new version has been uploaded + feed = source.GetReleaseFeed(logger, channel).GetAwaiterResult(); + latest = feed.Assets.Where(a => a.Version != null && a.Type == VelopackAssetType.Full) + .OrderByDescending(a => a.Version) + .FirstOrDefault(); + + Assert.True(latest != null, "No latest version found."); + Assert.Equal(newVer, latest.Version); + Assert.True(feed.Assets.Count(x => x.Type == VelopackAssetType.Full) <= options.KeepMaxReleases, "Too many releases were kept."); } } diff --git a/test/Velopack.Packaging.Tests/TestApp.cs b/test/Velopack.Packaging.Tests/TestApp.cs index 9fe5c012..223cb0cf 100644 --- a/test/Velopack.Packaging.Tests/TestApp.cs +++ b/test/Velopack.Packaging.Tests/TestApp.cs @@ -8,83 +8,82 @@ using Velopack.Packaging.Unix.Commands; using Velopack.Packaging.Windows.Commands; using Velopack.Vpk.Logging; -namespace Velopack.Packaging.Tests +namespace Velopack.Packaging.Tests; + +public static class TestApp { - public static class TestApp + public static void PackTestApp(string id, string version, string testString, string releaseDir, ILogger logger, + string releaseNotes = null, string channel = null) { - public static void PackTestApp(string id, string version, string testString, string releaseDir, ILogger logger, - string releaseNotes = null, string channel = null) - { - var projDir = PathHelper.GetTestRootPath("TestApp"); - var testStringFile = Path.Combine(projDir, "Const.cs"); - var oldText = File.ReadAllText(testStringFile); + var projDir = PathHelper.GetTestRootPath("TestApp"); + var testStringFile = Path.Combine(projDir, "Const.cs"); + var oldText = File.ReadAllText(testStringFile); - try { - File.WriteAllText(testStringFile, $"class Const {{ public const string TEST_STRING = \"{testString}\"; }}"); + try { + File.WriteAllText(testStringFile, $"class Const {{ public const string TEST_STRING = \"{testString}\"; }}"); - var args = new string[] { "publish", "--no-self-contained", "-c", "Release", "-r", VelopackRuntimeInfo.SystemRid, "-o", "publish" }; + var args = new string[] { "publish", "--no-self-contained", "-c", "Release", "-r", VelopackRuntimeInfo.SystemRid, "-o", "publish" }; - var psi = new ProcessStartInfo("dotnet"); - psi.WorkingDirectory = projDir; - psi.AppendArgumentListSafe(args, out var debug); + var psi = new ProcessStartInfo("dotnet"); + psi.WorkingDirectory = projDir; + psi.AppendArgumentListSafe(args, out var debug); - logger.Info($"TEST: Running {psi.FileName} {debug}"); + logger.Info($"TEST: Running {psi.FileName} {debug}"); - using var p = Process.Start(psi); - p.WaitForExit(); + using var p = Process.Start(psi); + p.WaitForExit(); - if (p.ExitCode != 0) - throw new Exception($"dotnet publish failed with exit code {p.ExitCode}"); + if (p.ExitCode != 0) + throw new Exception($"dotnet publish failed with exit code {p.ExitCode}"); - var console = new BasicConsole(logger, new DefaultPromptValueFactory(false)); + var console = new BasicConsole(logger, new DefaultPromptValueFactory(false)); - if (VelopackRuntimeInfo.IsWindows) { - var options = new WindowsPackOptions { - EntryExecutableName = "TestApp.exe", - ReleaseDir = new DirectoryInfo(releaseDir), - PackId = id, - TargetRuntime = RID.Parse(VelopackRuntimeInfo.SystemOs.GetOsShortName()), - PackVersion = version, - PackDirectory = Path.Combine(projDir, "publish"), - ReleaseNotes = releaseNotes, - Channel = channel, - }; - var runner = new WindowsPackCommandRunner(logger, console); - runner.Run(options).GetAwaiterResult(); - } else if (VelopackRuntimeInfo.IsOSX) { - var options = new OsxPackOptions { - EntryExecutableName = "TestApp", - ReleaseDir = new DirectoryInfo(releaseDir), - PackId = id, - Icon = Path.Combine(PathHelper.GetProjectDir(), "examples", "AvaloniaCrossPlat", "Velopack.icns"), - TargetRuntime = RID.Parse(VelopackRuntimeInfo.SystemOs.GetOsShortName()), - PackVersion = version, - PackDirectory = Path.Combine(projDir, "publish"), - ReleaseNotes = releaseNotes, - Channel = channel, - }; - var runner = new OsxPackCommandRunner(logger, console); - runner.Run(options).GetAwaiterResult(); - } else if (VelopackRuntimeInfo.IsLinux) { - var options = new LinuxPackOptions { - EntryExecutableName = "TestApp", - ReleaseDir = new DirectoryInfo(releaseDir), - PackId = id, - Icon = Path.Combine(PathHelper.GetProjectDir(), "examples", "AvaloniaCrossPlat", "Velopack.png"), - TargetRuntime = RID.Parse(VelopackRuntimeInfo.SystemOs.GetOsShortName()), - PackVersion = version, - PackDirectory = Path.Combine(projDir, "publish"), - ReleaseNotes = releaseNotes, - Channel = channel, - }; - var runner = new LinuxPackCommandRunner(logger, console); - runner.Run(options).GetAwaiterResult(); - } else { - throw new PlatformNotSupportedException(); - } - } finally { - File.WriteAllText(testStringFile, oldText); + if (VelopackRuntimeInfo.IsWindows) { + var options = new WindowsPackOptions { + EntryExecutableName = "TestApp.exe", + ReleaseDir = new DirectoryInfo(releaseDir), + PackId = id, + TargetRuntime = RID.Parse(VelopackRuntimeInfo.SystemOs.GetOsShortName()), + PackVersion = version, + PackDirectory = Path.Combine(projDir, "publish"), + ReleaseNotes = releaseNotes, + Channel = channel, + }; + var runner = new WindowsPackCommandRunner(logger, console); + runner.Run(options).GetAwaiterResult(); + } else if (VelopackRuntimeInfo.IsOSX) { + var options = new OsxPackOptions { + EntryExecutableName = "TestApp", + ReleaseDir = new DirectoryInfo(releaseDir), + PackId = id, + Icon = Path.Combine(PathHelper.GetProjectDir(), "examples", "AvaloniaCrossPlat", "Velopack.icns"), + TargetRuntime = RID.Parse(VelopackRuntimeInfo.SystemOs.GetOsShortName()), + PackVersion = version, + PackDirectory = Path.Combine(projDir, "publish"), + ReleaseNotes = releaseNotes, + Channel = channel, + }; + var runner = new OsxPackCommandRunner(logger, console); + runner.Run(options).GetAwaiterResult(); + } else if (VelopackRuntimeInfo.IsLinux) { + var options = new LinuxPackOptions { + EntryExecutableName = "TestApp", + ReleaseDir = new DirectoryInfo(releaseDir), + PackId = id, + Icon = Path.Combine(PathHelper.GetProjectDir(), "examples", "AvaloniaCrossPlat", "Velopack.png"), + TargetRuntime = RID.Parse(VelopackRuntimeInfo.SystemOs.GetOsShortName()), + PackVersion = version, + PackDirectory = Path.Combine(projDir, "publish"), + ReleaseNotes = releaseNotes, + Channel = channel, + }; + var runner = new LinuxPackCommandRunner(logger, console); + runner.Run(options).GetAwaiterResult(); + } else { + throw new PlatformNotSupportedException(); } + } finally { + File.WriteAllText(testStringFile, oldText); } } } diff --git a/test/Velopack.Tests/OldSquirrel/ReleaseEntry.cs b/test/Velopack.Tests/OldSquirrel/ReleaseEntry.cs index d8d405b9..496c2460 100644 --- a/test/Velopack.Tests/OldSquirrel/ReleaseEntry.cs +++ b/test/Velopack.Tests/OldSquirrel/ReleaseEntry.cs @@ -2,278 +2,277 @@ using System.Runtime.Serialization; using System.Text.RegularExpressions; -namespace Velopack.Tests.OldSquirrel +namespace Velopack.Tests.OldSquirrel; + +[DataContract] +public class ReleaseEntry { - [DataContract] - public class ReleaseEntry + [DataMember] public string SHA1 { get; protected set; } + [DataMember] public string BaseUrl { get; protected set; } + [DataMember] public string Filename { get; protected set; } + [DataMember] public string Query { get; protected set; } + [DataMember] public long Filesize { get; protected set; } + [DataMember] public bool IsDelta { get; protected set; } + [DataMember] public float? StagingPercentage { get; protected set; } + + protected ReleaseEntry(string sha1, string filename, long filesize, bool isDelta, string baseUrl = null, string query = null, float? stagingPercentage = null) { - [DataMember] public string SHA1 { get; protected set; } - [DataMember] public string BaseUrl { get; protected set; } - [DataMember] public string Filename { get; protected set; } - [DataMember] public string Query { get; protected set; } - [DataMember] public long Filesize { get; protected set; } - [DataMember] public bool IsDelta { get; protected set; } - [DataMember] public float? StagingPercentage { get; protected set; } + Contract.Requires(sha1 != null && sha1.Length == 40); + Contract.Requires(filename != null); + Contract.Requires(filename.Contains(Path.DirectorySeparatorChar) == false); + Contract.Requires(filesize > 0); - protected ReleaseEntry(string sha1, string filename, long filesize, bool isDelta, string baseUrl = null, string query = null, float? stagingPercentage = null) - { - Contract.Requires(sha1 != null && sha1.Length == 40); - Contract.Requires(filename != null); - Contract.Requires(filename.Contains(Path.DirectorySeparatorChar) == false); - Contract.Requires(filesize > 0); - - SHA1 = sha1; BaseUrl = baseUrl; Filename = filename; Query = query; Filesize = filesize; IsDelta = isDelta; StagingPercentage = stagingPercentage; - } - - [IgnoreDataMember] - public string EntryAsString { - get { - if (StagingPercentage != null) { - return String.Format("{0} {1}{2} {3} # {4}", SHA1, BaseUrl, Filename, Filesize, stagingPercentageAsString(StagingPercentage.Value)); - } else { - return String.Format("{0} {1}{2} {3}", SHA1, BaseUrl, Filename, Filesize); - } - } - } - - [IgnoreDataMember] - public SemanticVersion Version { get { return Filename.ToSemanticVersion(); } } - - static readonly Regex packageNameRegex = new Regex(@"^([\w-]+)-\d+\..+\.nupkg$"); - [IgnoreDataMember] - public string PackageName { - get { - var match = packageNameRegex.Match(Filename); - return match.Success ? - match.Groups[1].Value : - Filename.Substring(0, Filename.IndexOfAny(new[] { '-', '.' })); - } - } - - //public string GetReleaseNotes(string packageDirectory) - //{ - // var zp = new ZipPackage(Path.Combine(packageDirectory, Filename)); - // var t = zp.Id; - - // if (String.IsNullOrWhiteSpace(zp.ReleaseNotes)) { - // throw new Exception(String.Format("Invalid 'ReleaseNotes' value in nuspec file at '{0}'", Path.Combine(packageDirectory, Filename))); - // } - - // return zp.ReleaseNotes; - //} - - //public Uri GetIconUrl(string packageDirectory) - //{ - // var zp = new ZipPackage(Path.Combine(packageDirectory, Filename)); - // return zp.IconUrl; - //} - - static readonly Regex entryRegex = new Regex(@"^([0-9a-fA-F]{40})\s+(\S+)\s+(\d+)[\r]*$"); - static readonly Regex commentRegex = new Regex(@"\s*#.*$"); - static readonly Regex stagingRegex = new Regex(@"#\s+(\d{1,3})%$"); - public static ReleaseEntry ParseReleaseEntry(string entry) - { - Contract.Requires(entry != null); - - float? stagingPercentage = null; - var m = stagingRegex.Match(entry); - if (m != null && m.Success) { - stagingPercentage = Single.Parse(m.Groups[1].Value) / 100.0f; - } - - entry = commentRegex.Replace(entry, ""); - if (String.IsNullOrWhiteSpace(entry)) { - return null; - } - - m = entryRegex.Match(entry); - if (!m.Success) { - throw new Exception("Invalid release entry: " + entry); - } - - if (m.Groups.Count != 4) { - throw new Exception("Invalid release entry: " + entry); - } - - string filename = m.Groups[2].Value; - - // Split the base URL and the filename if an URI is provided, - // throws if a path is provided - string baseUrl = null; - string query = null; - - if (Utility.IsHttpUrl(filename)) { - var uri = new Uri(filename); - var path = uri.LocalPath; - var authority = uri.GetLeftPart(UriPartial.Authority); - - if (String.IsNullOrEmpty(path) || String.IsNullOrEmpty(authority)) { - throw new Exception("Invalid URL"); - } - - var indexOfLastPathSeparator = path.LastIndexOf("/") + 1; - baseUrl = authority + path.Substring(0, indexOfLastPathSeparator); - filename = path.Substring(indexOfLastPathSeparator); - - if (!String.IsNullOrEmpty(uri.Query)) { - query = uri.Query; - } - } - - if (filename.IndexOfAny(Path.GetInvalidFileNameChars()) > -1) { - throw new Exception("Filename can either be an absolute HTTP[s] URL, *or* a file name"); - } - - long size = Int64.Parse(m.Groups[3].Value); - bool isDelta = filenameIsDeltaFile(filename); - - return new ReleaseEntry(m.Groups[1].Value, filename, size, isDelta, baseUrl, query, stagingPercentage); - } - - public bool IsStagingMatch(Guid? userId) - { - // A "Staging match" is when a user falls into the affirmative - // bucket - i.e. if the staging is at 10%, this user is the one out - // of ten case. - if (!StagingPercentage.HasValue) return true; - if (!userId.HasValue) return false; - - uint val = BitConverter.ToUInt32(userId.Value.ToByteArray(), 12); - - double percentage = ((double) val / (double) UInt32.MaxValue); - return percentage < StagingPercentage.Value; - } - - public static IEnumerable ParseReleaseFile(string fileContents) - { - if (String.IsNullOrEmpty(fileContents)) { - return new ReleaseEntry[0]; - } - - //fileContents = Utility.RemoveByteOrderMarkerIfPresent(fileContents); - - var ret = fileContents.Split('\n') - .Where(x => !String.IsNullOrWhiteSpace(x)) - .Select(ParseReleaseEntry) - .Where(x => x != null) - .ToArray(); - - return ret.Any(x => x == null) ? null : ret; - } - - public static IEnumerable ParseReleaseFileAndApplyStaging(string fileContents, Guid? userToken) - { - if (String.IsNullOrEmpty(fileContents)) { - return new ReleaseEntry[0]; - } - - //fileContents = Utility.RemoveByteOrderMarkerIfPresent(fileContents); - - var ret = fileContents.Split('\n') - .Where(x => !String.IsNullOrWhiteSpace(x)) - .Select(ParseReleaseEntry) - .Where(x => x != null && x.IsStagingMatch(userToken)) - .ToArray(); - - return ret.Any(x => x == null) ? null : ret; - } - - - //public static void WriteReleaseFile(IEnumerable releaseEntries, Stream stream) - //{ - // Contract.Requires(releaseEntries != null && releaseEntries.Any()); - // Contract.Requires(stream != null); - - // using (var sw = new StreamWriter(stream, Encoding.UTF8)) { - // sw.Write(String.Join("\n", releaseEntries - // .OrderBy(x => x.Version) - // .ThenByDescending(x => x.IsDelta) - // .Select(x => x.EntryAsString))); - // } - //} - - //public static void WriteReleaseFile(IEnumerable releaseEntries, string path) - //{ - // Contract.Requires(releaseEntries != null && releaseEntries.Any()); - // Contract.Requires(!String.IsNullOrEmpty(path)); - - // using (var f = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None)) { - // WriteReleaseFile(releaseEntries, f); - // } - //} - - //public static ReleaseEntry GenerateFromFile(Stream file, string filename, string baseUrl = null) - //{ - // Contract.Requires(file != null && file.CanRead); - // Contract.Requires(!String.IsNullOrEmpty(filename)); - - // var hash = Utility.CalculateStreamSHA1(file); - // return new ReleaseEntry(hash, filename, file.Length, filenameIsDeltaFile(filename), baseUrl); - //} - - //public static ReleaseEntry GenerateFromFile(string path, string baseUrl = null) - //{ - // using (var inf = File.OpenRead(path)) { - // return GenerateFromFile(inf, Path.GetFileName(path), baseUrl); - // } - //} - - //public static List BuildReleasesFile(string releasePackagesDir) - //{ - // var packagesDir = new DirectoryInfo(releasePackagesDir); - - // // Generate release entries for all of the local packages - // var entriesQueue = new ConcurrentQueue(); - // Parallel.ForEach(packagesDir.GetFiles("*.nupkg"), x => { - // using (var file = x.OpenRead()) { - // entriesQueue.Enqueue(GenerateFromFile(file, x.Name)); - // } - // }); - - // // Write the new RELEASES file to a temp file then move it into - // // place - // var entries = entriesQueue.ToList(); - // var tempFile = default(string); - // Utility.WithTempFile(out tempFile, releasePackagesDir); - - // try { - // using (var of = File.OpenWrite(tempFile)) { - // if (entries.Count > 0) WriteReleaseFile(entries, of); - // } - - // var target = Path.Combine(packagesDir.FullName, "RELEASES"); - // if (File.Exists(target)) { - // File.Delete(target); - // } - - // File.Move(tempFile, target); - // } finally { - // if (File.Exists(tempFile)) Utility.DeleteFileHarder(tempFile, true); - // } - - // return entries; - //} - - static string stagingPercentageAsString(float percentage) - { - return String.Format("{0:F0}%", percentage * 100.0); - } - - static bool filenameIsDeltaFile(string filename) - { - return filename.EndsWith("-delta.nupkg", StringComparison.InvariantCultureIgnoreCase); - } - - //public static ReleasePackage GetPreviousRelease(IEnumerable releaseEntries, IReleasePackage package, string targetDir) - //{ - // if (releaseEntries == null || !releaseEntries.Any()) return null; - - // return releaseEntries - // .Where(x => x.IsDelta == false) - // .Where(x => x.Version < package.ToSemanticVersion()) - // .OrderByDescending(x => x.Version) - // .Select(x => new ReleasePackage(Path.Combine(targetDir, x.Filename), true)) - // .FirstOrDefault(); - //} + SHA1 = sha1; BaseUrl = baseUrl; Filename = filename; Query = query; Filesize = filesize; IsDelta = isDelta; StagingPercentage = stagingPercentage; } + + [IgnoreDataMember] + public string EntryAsString { + get { + if (StagingPercentage != null) { + return String.Format("{0} {1}{2} {3} # {4}", SHA1, BaseUrl, Filename, Filesize, stagingPercentageAsString(StagingPercentage.Value)); + } else { + return String.Format("{0} {1}{2} {3}", SHA1, BaseUrl, Filename, Filesize); + } + } + } + + [IgnoreDataMember] + public SemanticVersion Version { get { return Filename.ToSemanticVersion(); } } + + static readonly Regex packageNameRegex = new Regex(@"^([\w-]+)-\d+\..+\.nupkg$"); + [IgnoreDataMember] + public string PackageName { + get { + var match = packageNameRegex.Match(Filename); + return match.Success ? + match.Groups[1].Value : + Filename.Substring(0, Filename.IndexOfAny(new[] { '-', '.' })); + } + } + + //public string GetReleaseNotes(string packageDirectory) + //{ + // var zp = new ZipPackage(Path.Combine(packageDirectory, Filename)); + // var t = zp.Id; + + // if (String.IsNullOrWhiteSpace(zp.ReleaseNotes)) { + // throw new Exception(String.Format("Invalid 'ReleaseNotes' value in nuspec file at '{0}'", Path.Combine(packageDirectory, Filename))); + // } + + // return zp.ReleaseNotes; + //} + + //public Uri GetIconUrl(string packageDirectory) + //{ + // var zp = new ZipPackage(Path.Combine(packageDirectory, Filename)); + // return zp.IconUrl; + //} + + static readonly Regex entryRegex = new Regex(@"^([0-9a-fA-F]{40})\s+(\S+)\s+(\d+)[\r]*$"); + static readonly Regex commentRegex = new Regex(@"\s*#.*$"); + static readonly Regex stagingRegex = new Regex(@"#\s+(\d{1,3})%$"); + public static ReleaseEntry ParseReleaseEntry(string entry) + { + Contract.Requires(entry != null); + + float? stagingPercentage = null; + var m = stagingRegex.Match(entry); + if (m != null && m.Success) { + stagingPercentage = Single.Parse(m.Groups[1].Value) / 100.0f; + } + + entry = commentRegex.Replace(entry, ""); + if (String.IsNullOrWhiteSpace(entry)) { + return null; + } + + m = entryRegex.Match(entry); + if (!m.Success) { + throw new Exception("Invalid release entry: " + entry); + } + + if (m.Groups.Count != 4) { + throw new Exception("Invalid release entry: " + entry); + } + + string filename = m.Groups[2].Value; + + // Split the base URL and the filename if an URI is provided, + // throws if a path is provided + string baseUrl = null; + string query = null; + + if (Utility.IsHttpUrl(filename)) { + var uri = new Uri(filename); + var path = uri.LocalPath; + var authority = uri.GetLeftPart(UriPartial.Authority); + + if (String.IsNullOrEmpty(path) || String.IsNullOrEmpty(authority)) { + throw new Exception("Invalid URL"); + } + + var indexOfLastPathSeparator = path.LastIndexOf("/") + 1; + baseUrl = authority + path.Substring(0, indexOfLastPathSeparator); + filename = path.Substring(indexOfLastPathSeparator); + + if (!String.IsNullOrEmpty(uri.Query)) { + query = uri.Query; + } + } + + if (filename.IndexOfAny(Path.GetInvalidFileNameChars()) > -1) { + throw new Exception("Filename can either be an absolute HTTP[s] URL, *or* a file name"); + } + + long size = Int64.Parse(m.Groups[3].Value); + bool isDelta = filenameIsDeltaFile(filename); + + return new ReleaseEntry(m.Groups[1].Value, filename, size, isDelta, baseUrl, query, stagingPercentage); + } + + public bool IsStagingMatch(Guid? userId) + { + // A "Staging match" is when a user falls into the affirmative + // bucket - i.e. if the staging is at 10%, this user is the one out + // of ten case. + if (!StagingPercentage.HasValue) return true; + if (!userId.HasValue) return false; + + uint val = BitConverter.ToUInt32(userId.Value.ToByteArray(), 12); + + double percentage = ((double) val / (double) UInt32.MaxValue); + return percentage < StagingPercentage.Value; + } + + public static IEnumerable ParseReleaseFile(string fileContents) + { + if (String.IsNullOrEmpty(fileContents)) { + return new ReleaseEntry[0]; + } + + //fileContents = Utility.RemoveByteOrderMarkerIfPresent(fileContents); + + var ret = fileContents.Split('\n') + .Where(x => !String.IsNullOrWhiteSpace(x)) + .Select(ParseReleaseEntry) + .Where(x => x != null) + .ToArray(); + + return ret.Any(x => x == null) ? null : ret; + } + + public static IEnumerable ParseReleaseFileAndApplyStaging(string fileContents, Guid? userToken) + { + if (String.IsNullOrEmpty(fileContents)) { + return new ReleaseEntry[0]; + } + + //fileContents = Utility.RemoveByteOrderMarkerIfPresent(fileContents); + + var ret = fileContents.Split('\n') + .Where(x => !String.IsNullOrWhiteSpace(x)) + .Select(ParseReleaseEntry) + .Where(x => x != null && x.IsStagingMatch(userToken)) + .ToArray(); + + return ret.Any(x => x == null) ? null : ret; + } + + + //public static void WriteReleaseFile(IEnumerable releaseEntries, Stream stream) + //{ + // Contract.Requires(releaseEntries != null && releaseEntries.Any()); + // Contract.Requires(stream != null); + + // using (var sw = new StreamWriter(stream, Encoding.UTF8)) { + // sw.Write(String.Join("\n", releaseEntries + // .OrderBy(x => x.Version) + // .ThenByDescending(x => x.IsDelta) + // .Select(x => x.EntryAsString))); + // } + //} + + //public static void WriteReleaseFile(IEnumerable releaseEntries, string path) + //{ + // Contract.Requires(releaseEntries != null && releaseEntries.Any()); + // Contract.Requires(!String.IsNullOrEmpty(path)); + + // using (var f = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.None)) { + // WriteReleaseFile(releaseEntries, f); + // } + //} + + //public static ReleaseEntry GenerateFromFile(Stream file, string filename, string baseUrl = null) + //{ + // Contract.Requires(file != null && file.CanRead); + // Contract.Requires(!String.IsNullOrEmpty(filename)); + + // var hash = Utility.CalculateStreamSHA1(file); + // return new ReleaseEntry(hash, filename, file.Length, filenameIsDeltaFile(filename), baseUrl); + //} + + //public static ReleaseEntry GenerateFromFile(string path, string baseUrl = null) + //{ + // using (var inf = File.OpenRead(path)) { + // return GenerateFromFile(inf, Path.GetFileName(path), baseUrl); + // } + //} + + //public static List BuildReleasesFile(string releasePackagesDir) + //{ + // var packagesDir = new DirectoryInfo(releasePackagesDir); + + // // Generate release entries for all of the local packages + // var entriesQueue = new ConcurrentQueue(); + // Parallel.ForEach(packagesDir.GetFiles("*.nupkg"), x => { + // using (var file = x.OpenRead()) { + // entriesQueue.Enqueue(GenerateFromFile(file, x.Name)); + // } + // }); + + // // Write the new RELEASES file to a temp file then move it into + // // place + // var entries = entriesQueue.ToList(); + // var tempFile = default(string); + // Utility.WithTempFile(out tempFile, releasePackagesDir); + + // try { + // using (var of = File.OpenWrite(tempFile)) { + // if (entries.Count > 0) WriteReleaseFile(entries, of); + // } + + // var target = Path.Combine(packagesDir.FullName, "RELEASES"); + // if (File.Exists(target)) { + // File.Delete(target); + // } + + // File.Move(tempFile, target); + // } finally { + // if (File.Exists(tempFile)) Utility.DeleteFileHarder(tempFile, true); + // } + + // return entries; + //} + + static string stagingPercentageAsString(float percentage) + { + return String.Format("{0:F0}%", percentage * 100.0); + } + + static bool filenameIsDeltaFile(string filename) + { + return filename.EndsWith("-delta.nupkg", StringComparison.InvariantCultureIgnoreCase); + } + + //public static ReleasePackage GetPreviousRelease(IEnumerable releaseEntries, IReleasePackage package, string targetDir) + //{ + // if (releaseEntries == null || !releaseEntries.Any()) return null; + + // return releaseEntries + // .Where(x => x.IsDelta == false) + // .Where(x => x.Version < package.ToSemanticVersion()) + // .OrderByDescending(x => x.Version) + // .Select(x => new ReleasePackage(Path.Combine(targetDir, x.Filename), true)) + // .FirstOrDefault(); + //} } \ No newline at end of file diff --git a/test/Velopack.Tests/OldSquirrel/ReleaseExtensions.cs b/test/Velopack.Tests/OldSquirrel/ReleaseExtensions.cs index 3ea46d41..f0d121f3 100644 --- a/test/Velopack.Tests/OldSquirrel/ReleaseExtensions.cs +++ b/test/Velopack.Tests/OldSquirrel/ReleaseExtensions.cs @@ -1,22 +1,21 @@ using System.Text.RegularExpressions; -namespace Velopack.Tests.OldSquirrel +namespace Velopack.Tests.OldSquirrel; + +public static class VersionExtensions { - public static class VersionExtensions + static readonly Regex _suffixRegex = new Regex(@"(-full|-delta)?\.nupkg$", RegexOptions.Compiled); + static readonly Regex _versionRegex = new Regex(@"\d+(\.\d+){0,3}(-[A-Za-z][0-9A-Za-z-]*)?$", RegexOptions.Compiled); + + //public static SemanticVersion ToSemanticVersion(this IReleasePackage package) + //{ + // return package.InputPackageFile.ToSemanticVersion(); + //} + + public static SemanticVersion ToSemanticVersion(this string fileName) { - static readonly Regex _suffixRegex = new Regex(@"(-full|-delta)?\.nupkg$", RegexOptions.Compiled); - static readonly Regex _versionRegex = new Regex(@"\d+(\.\d+){0,3}(-[A-Za-z][0-9A-Za-z-]*)?$", RegexOptions.Compiled); - - //public static SemanticVersion ToSemanticVersion(this IReleasePackage package) - //{ - // return package.InputPackageFile.ToSemanticVersion(); - //} - - public static SemanticVersion ToSemanticVersion(this string fileName) - { - var name = _suffixRegex.Replace(fileName, ""); - var version = _versionRegex.Match(name).Value; - return new SemanticVersion(version); - } + var name = _suffixRegex.Replace(fileName, ""); + var version = _versionRegex.Match(name).Value; + return new SemanticVersion(version); } } \ No newline at end of file diff --git a/test/Velopack.Tests/OldSquirrel/SemanticVersion.cs b/test/Velopack.Tests/OldSquirrel/SemanticVersion.cs index 5e0a2bda..9bf2d291 100644 --- a/test/Velopack.Tests/OldSquirrel/SemanticVersion.cs +++ b/test/Velopack.Tests/OldSquirrel/SemanticVersion.cs @@ -1,302 +1,301 @@ using System.Globalization; using System.Text.RegularExpressions; -namespace Velopack.Tests.OldSquirrel +namespace Velopack.Tests.OldSquirrel; + +/// +/// A hybrid implementation of SemVer that supports semantic versioning as described at http://semver.org while not strictly enforcing it to +/// allow older 4-digit versioning schemes to continue working. +/// +[Serializable] +public sealed class SemanticVersion : IComparable, IComparable, IEquatable { - /// - /// A hybrid implementation of SemVer that supports semantic versioning as described at http://semver.org while not strictly enforcing it to - /// allow older 4-digit versioning schemes to continue working. - /// - [Serializable] - public sealed class SemanticVersion : IComparable, IComparable, IEquatable + private const RegexOptions _flags = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; + private static readonly Regex _semanticVersionRegex = new Regex(@"^(?\d+(\s*\.\s*\d+){0,3})(?-[a-z][0-9a-z-]*)?$", _flags); + private static readonly Regex _strictSemanticVersionRegex = new Regex(@"^(?\d+(\.\d+){2})(?-[a-z][0-9a-z-]*)?$", _flags); + private static readonly Regex _preReleaseVersionRegex = new Regex(@"(?[a-z]+)(?[0-9]+)$", _flags); + private readonly string _originalString; + + public SemanticVersion(string version) + : this(Parse(version)) { - private const RegexOptions _flags = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; - private static readonly Regex _semanticVersionRegex = new Regex(@"^(?\d+(\s*\.\s*\d+){0,3})(?-[a-z][0-9a-z-]*)?$", _flags); - private static readonly Regex _strictSemanticVersionRegex = new Regex(@"^(?\d+(\.\d+){2})(?-[a-z][0-9a-z-]*)?$", _flags); - private static readonly Regex _preReleaseVersionRegex = new Regex(@"(?[a-z]+)(?[0-9]+)$", _flags); - private readonly string _originalString; + // The constructor normalizes the version string so that it we do not need to normalize it every time we need to operate on it. + // The original string represents the original form in which the version is represented to be used when printing. + _originalString = version; + } - public SemanticVersion(string version) - : this(Parse(version)) - { - // The constructor normalizes the version string so that it we do not need to normalize it every time we need to operate on it. - // The original string represents the original form in which the version is represented to be used when printing. - _originalString = version; + public SemanticVersion(int major, int minor, int build, int revision) + : this(new Version(major, minor, build, revision)) + { + } + + public SemanticVersion(int major, int minor, int build, string specialVersion) + : this(new Version(major, minor, build), specialVersion) + { + } + + public SemanticVersion(Version version) + : this(version, String.Empty) + { + } + + public SemanticVersion(Version version, string specialVersion) + : this(version, specialVersion, null) + { + } + + private SemanticVersion(Version version, string specialVersion, string originalString) + { + if (version == null) { + throw new ArgumentNullException("version"); } + Version = NormalizeVersionValue(version); + SpecialVersion = specialVersion ?? String.Empty; + _originalString = String.IsNullOrEmpty(originalString) ? version.ToString() + (!String.IsNullOrEmpty(specialVersion) ? '-' + specialVersion : null) : originalString; + } - public SemanticVersion(int major, int minor, int build, int revision) - : this(new Version(major, minor, build, revision)) - { - } + internal SemanticVersion(SemanticVersion semVer) + { + _originalString = semVer.ToString(); + Version = semVer.Version; + SpecialVersion = semVer.SpecialVersion; + } - public SemanticVersion(int major, int minor, int build, string specialVersion) - : this(new Version(major, minor, build), specialVersion) - { - } + /// + /// Gets the normalized version portion. + /// + public Version Version { + get; + private set; + } - public SemanticVersion(Version version) - : this(version, String.Empty) - { - } + /// + /// Gets the optional special version. + /// + public string SpecialVersion { + get; + private set; + } - public SemanticVersion(Version version, string specialVersion) - : this(version, specialVersion, null) - { - } + public string[] GetOriginalVersionComponents() + { + if (!String.IsNullOrEmpty(_originalString)) { + string original; - private SemanticVersion(Version version, string specialVersion, string originalString) - { - if (version == null) { - throw new ArgumentNullException("version"); - } - Version = NormalizeVersionValue(version); - SpecialVersion = specialVersion ?? String.Empty; - _originalString = String.IsNullOrEmpty(originalString) ? version.ToString() + (!String.IsNullOrEmpty(specialVersion) ? '-' + specialVersion : null) : originalString; - } - - internal SemanticVersion(SemanticVersion semVer) - { - _originalString = semVer.ToString(); - Version = semVer.Version; - SpecialVersion = semVer.SpecialVersion; - } - - /// - /// Gets the normalized version portion. - /// - public Version Version { - get; - private set; - } - - /// - /// Gets the optional special version. - /// - public string SpecialVersion { - get; - private set; - } - - public string[] GetOriginalVersionComponents() - { - if (!String.IsNullOrEmpty(_originalString)) { - string original; - - // search the start of the SpecialVersion part, if any - int dashIndex = _originalString.IndexOf('-'); - if (dashIndex != -1) { - // remove the SpecialVersion part - original = _originalString.Substring(0, dashIndex); - } else { - original = _originalString; - } - - return SplitAndPadVersionString(original); + // search the start of the SpecialVersion part, if any + int dashIndex = _originalString.IndexOf('-'); + if (dashIndex != -1) { + // remove the SpecialVersion part + original = _originalString.Substring(0, dashIndex); } else { - return SplitAndPadVersionString(Version.ToString()); - } - } - - private static string[] SplitAndPadVersionString(string version) - { - string[] a = version.Split('.'); - if (a.Length == 4) { - return a; - } else { - // if 'a' has less than 4 elements, we pad the '0' at the end - // to make it 4. - var b = new string[4] { "0", "0", "0", "0" }; - Array.Copy(a, 0, b, 0, a.Length); - return b; - } - } - - /// - /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. - /// - public static SemanticVersion Parse(string version) - { - if (String.IsNullOrEmpty(version)) { - throw new ArgumentException("Argument_Cannot_Be_Null_Or_Empty", "version"); + original = _originalString; } - SemanticVersion semVer; - if (!TryParse(version, out semVer)) { - throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, "InvalidVersionString", version), "version"); - } - return semVer; - } - - /// - /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. - /// - public static bool TryParse(string version, out SemanticVersion value) - { - return TryParseInternal(version, _semanticVersionRegex, out value); - } - - /// - /// Parses a version string using strict semantic versioning rules that allows exactly 3 components and an optional special version. - /// - public static bool TryParseStrict(string version, out SemanticVersion value) - { - return TryParseInternal(version, _strictSemanticVersionRegex, out value); - } - - private static bool TryParseInternal(string version, Regex regex, out SemanticVersion semVer) - { - semVer = null; - if (String.IsNullOrEmpty(version)) { - return false; - } - - var match = regex.Match(version.Trim()); - Version versionValue; - if (!match.Success || !Version.TryParse(match.Groups["Version"].Value, out versionValue)) { - return false; - } - - semVer = new SemanticVersion(NormalizeVersionValue(versionValue), match.Groups["Release"].Value.TrimStart('-'), version.Replace(" ", "")); - return true; - } - - /// - /// Attempts to parse the version token as a SemanticVersion. - /// - /// An instance of SemanticVersion if it parses correctly, null otherwise. - public static SemanticVersion ParseOptionalVersion(string version) - { - SemanticVersion semVer; - TryParse(version, out semVer); - return semVer; - } - - private static Version NormalizeVersionValue(Version version) - { - return new Version(version.Major, - version.Minor, - Math.Max(version.Build, 0), - Math.Max(version.Revision, 0)); - } - - public int CompareTo(object obj) - { - if (Object.ReferenceEquals(obj, null)) { - return 1; - } - SemanticVersion other = obj as SemanticVersion; - if (other == null) { - throw new ArgumentException("TypeMustBeASemanticVersion", "obj"); - } - return CompareTo(other); - } - - public int CompareTo(SemanticVersion other) - { - if (Object.ReferenceEquals(other, null)) { - return 1; - } - - int result = Version.CompareTo(other.Version); - - if (result != 0) { - return result; - } - - bool empty = String.IsNullOrEmpty(SpecialVersion); - bool otherEmpty = String.IsNullOrEmpty(other.SpecialVersion); - if (empty && otherEmpty) { - return 0; - } else if (empty) { - return 1; - } else if (otherEmpty) { - return -1; - } - - // If both versions have a prerelease section with the same prefix - // and end with digits, compare based on the digits' numeric order - var match = _preReleaseVersionRegex.Match(SpecialVersion.Trim()); - var otherMatch = _preReleaseVersionRegex.Match(other.SpecialVersion.Trim()); - if (match.Success && otherMatch.Success && - string.Equals( - match.Groups["PreReleaseString"].Value, - otherMatch.Groups["PreReleaseString"].Value, - StringComparison.OrdinalIgnoreCase)) { - int delta = - int.Parse(match.Groups["PreReleaseNumber"].Value) - - int.Parse(otherMatch.Groups["PreReleaseNumber"].Value); - - return delta != 0 ? delta / Math.Abs(delta) : 0; - } - - return StringComparer.OrdinalIgnoreCase.Compare(SpecialVersion, other.SpecialVersion); - } - - public static bool operator ==(SemanticVersion version1, SemanticVersion version2) - { - if (Object.ReferenceEquals(version1, null)) { - return Object.ReferenceEquals(version2, null); - } - return version1.Equals(version2); - } - - public static bool operator !=(SemanticVersion version1, SemanticVersion version2) - { - return !(version1 == version2); - } - - public static bool operator <(SemanticVersion version1, SemanticVersion version2) - { - if (version1 == null) { - throw new ArgumentNullException("version1"); - } - return version1.CompareTo(version2) < 0; - } - - public static bool operator <=(SemanticVersion version1, SemanticVersion version2) - { - return (version1 == version2) || (version1 < version2); - } - - public static bool operator >(SemanticVersion version1, SemanticVersion version2) - { - if (version1 == null) { - throw new ArgumentNullException("version1"); - } - return version2 < version1; - } - - public static bool operator >=(SemanticVersion version1, SemanticVersion version2) - { - return (version1 == version2) || (version1 > version2); - } - - public override string ToString() - { - return _originalString; - } - - public bool Equals(SemanticVersion other) - { - return !Object.ReferenceEquals(null, other) && - Version.Equals(other.Version) && - SpecialVersion.Equals(other.SpecialVersion, StringComparison.OrdinalIgnoreCase); - } - - public override bool Equals(object obj) - { - SemanticVersion semVer = obj as SemanticVersion; - return !Object.ReferenceEquals(null, semVer) && Equals(semVer); - } - - public override int GetHashCode() - { - int hashCode = Version.GetHashCode(); - if (SpecialVersion != null) { - hashCode = hashCode * 4567 + SpecialVersion.GetHashCode(); - } - - return hashCode; + return SplitAndPadVersionString(original); + } else { + return SplitAndPadVersionString(Version.ToString()); } } + + private static string[] SplitAndPadVersionString(string version) + { + string[] a = version.Split('.'); + if (a.Length == 4) { + return a; + } else { + // if 'a' has less than 4 elements, we pad the '0' at the end + // to make it 4. + var b = new string[4] { "0", "0", "0", "0" }; + Array.Copy(a, 0, b, 0, a.Length); + return b; + } + } + + /// + /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. + /// + public static SemanticVersion Parse(string version) + { + if (String.IsNullOrEmpty(version)) { + throw new ArgumentException("Argument_Cannot_Be_Null_Or_Empty", "version"); + } + + SemanticVersion semVer; + if (!TryParse(version, out semVer)) { + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, "InvalidVersionString", version), "version"); + } + return semVer; + } + + /// + /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. + /// + public static bool TryParse(string version, out SemanticVersion value) + { + return TryParseInternal(version, _semanticVersionRegex, out value); + } + + /// + /// Parses a version string using strict semantic versioning rules that allows exactly 3 components and an optional special version. + /// + public static bool TryParseStrict(string version, out SemanticVersion value) + { + return TryParseInternal(version, _strictSemanticVersionRegex, out value); + } + + private static bool TryParseInternal(string version, Regex regex, out SemanticVersion semVer) + { + semVer = null; + if (String.IsNullOrEmpty(version)) { + return false; + } + + var match = regex.Match(version.Trim()); + Version versionValue; + if (!match.Success || !Version.TryParse(match.Groups["Version"].Value, out versionValue)) { + return false; + } + + semVer = new SemanticVersion(NormalizeVersionValue(versionValue), match.Groups["Release"].Value.TrimStart('-'), version.Replace(" ", "")); + return true; + } + + /// + /// Attempts to parse the version token as a SemanticVersion. + /// + /// An instance of SemanticVersion if it parses correctly, null otherwise. + public static SemanticVersion ParseOptionalVersion(string version) + { + SemanticVersion semVer; + TryParse(version, out semVer); + return semVer; + } + + private static Version NormalizeVersionValue(Version version) + { + return new Version(version.Major, + version.Minor, + Math.Max(version.Build, 0), + Math.Max(version.Revision, 0)); + } + + public int CompareTo(object obj) + { + if (Object.ReferenceEquals(obj, null)) { + return 1; + } + SemanticVersion other = obj as SemanticVersion; + if (other == null) { + throw new ArgumentException("TypeMustBeASemanticVersion", "obj"); + } + return CompareTo(other); + } + + public int CompareTo(SemanticVersion other) + { + if (Object.ReferenceEquals(other, null)) { + return 1; + } + + int result = Version.CompareTo(other.Version); + + if (result != 0) { + return result; + } + + bool empty = String.IsNullOrEmpty(SpecialVersion); + bool otherEmpty = String.IsNullOrEmpty(other.SpecialVersion); + if (empty && otherEmpty) { + return 0; + } else if (empty) { + return 1; + } else if (otherEmpty) { + return -1; + } + + // If both versions have a prerelease section with the same prefix + // and end with digits, compare based on the digits' numeric order + var match = _preReleaseVersionRegex.Match(SpecialVersion.Trim()); + var otherMatch = _preReleaseVersionRegex.Match(other.SpecialVersion.Trim()); + if (match.Success && otherMatch.Success && + string.Equals( + match.Groups["PreReleaseString"].Value, + otherMatch.Groups["PreReleaseString"].Value, + StringComparison.OrdinalIgnoreCase)) { + int delta = + int.Parse(match.Groups["PreReleaseNumber"].Value) - + int.Parse(otherMatch.Groups["PreReleaseNumber"].Value); + + return delta != 0 ? delta / Math.Abs(delta) : 0; + } + + return StringComparer.OrdinalIgnoreCase.Compare(SpecialVersion, other.SpecialVersion); + } + + public static bool operator ==(SemanticVersion version1, SemanticVersion version2) + { + if (Object.ReferenceEquals(version1, null)) { + return Object.ReferenceEquals(version2, null); + } + return version1.Equals(version2); + } + + public static bool operator !=(SemanticVersion version1, SemanticVersion version2) + { + return !(version1 == version2); + } + + public static bool operator <(SemanticVersion version1, SemanticVersion version2) + { + if (version1 == null) { + throw new ArgumentNullException("version1"); + } + return version1.CompareTo(version2) < 0; + } + + public static bool operator <=(SemanticVersion version1, SemanticVersion version2) + { + return (version1 == version2) || (version1 < version2); + } + + public static bool operator >(SemanticVersion version1, SemanticVersion version2) + { + if (version1 == null) { + throw new ArgumentNullException("version1"); + } + return version2 < version1; + } + + public static bool operator >=(SemanticVersion version1, SemanticVersion version2) + { + return (version1 == version2) || (version1 > version2); + } + + public override string ToString() + { + return _originalString; + } + + public bool Equals(SemanticVersion other) + { + return !Object.ReferenceEquals(null, other) && + Version.Equals(other.Version) && + SpecialVersion.Equals(other.SpecialVersion, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object obj) + { + SemanticVersion semVer = obj as SemanticVersion; + return !Object.ReferenceEquals(null, semVer) && Equals(semVer); + } + + public override int GetHashCode() + { + int hashCode = Version.GetHashCode(); + if (SpecialVersion != null) { + hashCode = hashCode * 4567 + SpecialVersion.GetHashCode(); + } + + return hashCode; + } } \ No newline at end of file diff --git a/test/Velopack.Tests/OldSquirrel/Utility.cs b/test/Velopack.Tests/OldSquirrel/Utility.cs index 8b624ee5..b16df9af 100644 --- a/test/Velopack.Tests/OldSquirrel/Utility.cs +++ b/test/Velopack.Tests/OldSquirrel/Utility.cs @@ -1,15 +1,14 @@ -namespace Velopack.Tests.OldSquirrel -{ - internal static class Utility - { - public static bool IsHttpUrl(string urlOrPath) - { - var uri = default(Uri); - if (!Uri.TryCreate(urlOrPath, UriKind.Absolute, out uri)) { - return false; - } +namespace Velopack.Tests.OldSquirrel; - return uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps; +internal static class Utility +{ + public static bool IsHttpUrl(string urlOrPath) + { + var uri = default(Uri); + if (!Uri.TryCreate(urlOrPath, UriKind.Absolute, out uri)) { + return false; } + + return uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps; } } diff --git a/test/Velopack.Tests/ReleaseEntryTests.cs b/test/Velopack.Tests/ReleaseEntryTests.cs index 716d2603..73c2623c 100644 --- a/test/Velopack.Tests/ReleaseEntryTests.cs +++ b/test/Velopack.Tests/ReleaseEntryTests.cs @@ -4,506 +4,505 @@ using NuGet.Versioning; using OldReleaseEntry = Velopack.Tests.OldSquirrel.ReleaseEntry; using OldSemanticVersion = Velopack.Tests.OldSquirrel.SemanticVersion; -namespace Velopack.Tests +namespace Velopack.Tests; + +public class ReleaseEntryTests { - public class ReleaseEntryTests + [Theory] + [InlineData(@"MyCoolApp-1.0-full.nupkg", "MyCoolApp", "1.0", "")] + [InlineData(@"MyCoolApp-1.0.0-full.nupkg", "MyCoolApp", "1.0.0", "")] + [InlineData(@"MyCoolApp-1.0.0-delta.nupkg", "MyCoolApp", "1.0.0", "")] + [InlineData(@"MyCoolApp-1.0.0-win-x64-full.nupkg", "MyCoolApp", "1.0.0", "win-x64")] + [InlineData(@"MyCoolApp-123.456.789-win-x64-full.nupkg", "MyCoolApp", "123.456.789", "win-x64")] + [InlineData(@"MyCoolApp-123.456.789-hello-win-x64-full.nupkg", "MyCoolApp", "123.456.789", "hello-win-x64")] + public void NewEntryCanRoundTripToOldSquirrel(string fileName, string id, string version, string metadata) { - [Theory] - [InlineData(@"MyCoolApp-1.0-full.nupkg", "MyCoolApp", "1.0", "")] - [InlineData(@"MyCoolApp-1.0.0-full.nupkg", "MyCoolApp", "1.0.0", "")] - [InlineData(@"MyCoolApp-1.0.0-delta.nupkg", "MyCoolApp", "1.0.0", "")] - [InlineData(@"MyCoolApp-1.0.0-win-x64-full.nupkg", "MyCoolApp", "1.0.0", "win-x64")] - [InlineData(@"MyCoolApp-123.456.789-win-x64-full.nupkg", "MyCoolApp", "123.456.789", "win-x64")] - [InlineData(@"MyCoolApp-123.456.789-hello-win-x64-full.nupkg", "MyCoolApp", "123.456.789", "hello-win-x64")] - public void NewEntryCanRoundTripToOldSquirrel(string fileName, string id, string version, string metadata) - { - var size = 80396; - var sha = "14db31d2647c6d2284882a2e101924a9c409ee67"; - var re = new ReleaseEntry(sha, fileName, size, null, null, null); - StringBuilder file = new StringBuilder(); - file.AppendLine(re.EntryAsString); + var size = 80396; + var sha = "14db31d2647c6d2284882a2e101924a9c409ee67"; + var re = new ReleaseEntry(sha, fileName, size, null, null, null); + StringBuilder file = new StringBuilder(); + file.AppendLine(re.EntryAsString); - var parsed = OldReleaseEntry.ParseReleaseFile(file.ToString()); - Assert.True(parsed.Count() == 1); + var parsed = OldReleaseEntry.ParseReleaseFile(file.ToString()); + Assert.True(parsed.Count() == 1); - var oldEntry = parsed.First(); + var oldEntry = parsed.First(); - Assert.Equal(fileName, oldEntry.Filename); - Assert.Equal(id, oldEntry.PackageName); - Assert.Equal(size, oldEntry.Filesize); - Assert.Equal(sha, oldEntry.SHA1); - Assert.Null(oldEntry.BaseUrl); - Assert.Null(oldEntry.Query); - Assert.True(oldEntry.Version.Version == OldSemanticVersion.Parse(version).Version); - Assert.Equal(oldEntry.Version.SpecialVersion, metadata); + Assert.Equal(fileName, oldEntry.Filename); + Assert.Equal(id, oldEntry.PackageName); + Assert.Equal(size, oldEntry.Filesize); + Assert.Equal(sha, oldEntry.SHA1); + Assert.Null(oldEntry.BaseUrl); + Assert.Null(oldEntry.Query); + Assert.True(oldEntry.Version.Version == OldSemanticVersion.Parse(version).Version); + Assert.Equal(oldEntry.Version.SpecialVersion, metadata); + } + + [Theory] + [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec MyCoolApp-1.0.nupkg 1004502", "MyCoolApp-1.0.nupkg", 1004502, null, null)] + [InlineData(@"3a2eadd15dd984e4559f2b4d790ec8badaeb6a39 MyCoolApp-1.1.nupkg 1040561", "MyCoolApp-1.1.nupkg", 1040561, null, null)] + [InlineData(@"14db31d2647c6d2284882a2e101924a9c409ee67 MyCoolApp-1.1.nupkg.delta 80396", "MyCoolApp-1.1.nupkg.delta", 80396, null, null)] + [InlineData(@"0000000000000000000000000000000000000000 http://test.org/Folder/MyCoolApp-1.2.nupkg 2569", "MyCoolApp-1.2.nupkg", 2569, "http://test.org/Folder/", null)] + [InlineData(@"0000000000000000000000000000000000000000 http://test.org/Folder/MyCoolApp-1.2.nupkg?query=param 2569", "MyCoolApp-1.2.nupkg", 2569, "http://test.org/Folder/", "?query=param")] + [InlineData(@"0000000000000000000000000000000000000000 https://www.test.org/Folder/MyCoolApp-1.2-delta.nupkg 1231953", "MyCoolApp-1.2-delta.nupkg", 1231953, "https://www.test.org/Folder/", null)] + [InlineData(@"0000000000000000000000000000000000000000 https://www.test.org/Folder/MyCoolApp-1.2-delta.nupkg?query=param 1231953", "MyCoolApp-1.2-delta.nupkg", 1231953, "https://www.test.org/Folder/", "?query=param")] + public void ParseValidReleaseEntryLines(string releaseEntry, string fileName, long fileSize, string baseUrl, string query) + { + var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); + + Assert.Equal(fileName, fixture.OriginalFilename); + Assert.Equal(fileSize, fixture.Filesize); + Assert.Equal(baseUrl, fixture.BaseUrl); + Assert.Equal(query, fixture.Query); + + var old = OldReleaseEntry.ParseReleaseEntry(releaseEntry); + Assert.Equal(fileName, old.Filename); + Assert.Equal(fileSize, old.Filesize); + Assert.Equal(baseUrl, old.BaseUrl); + Assert.Equal(query, old.Query); + } + + [Theory] + [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec My.Cool.App-1.0-full.nupkg 1004502", "My.Cool.App")] + [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec My.Cool.App-1.1.nupkg 1004502", "My.Cool.App")] + [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec http://test.org/Folder/My.Cool.App-1.2.nupkg?query=param 1231953", "My.Cool.App")] + public void ParseValidReleaseEntryLinesWithDots(string releaseEntry, string packageName) + { + var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); + Assert.Equal(packageName, fixture.PackageId); + } + + [Theory] + [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec My-Cool-App-1.0-full.nupkg 1004502", "My-Cool-App")] + [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec My.Cool-App-1.1.nupkg 1004502", "My.Cool-App")] + [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec http://test.org/Folder/My.Cool-App-1.2.nupkg?query=param 1231953", "My.Cool-App")] + public void ParseValidReleaseEntryLinesWithDashes(string releaseEntry, string packageName) + { + var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); + Assert.Equal(packageName, fixture.PackageId); + } + + [Theory] + [InlineData(@"0000000000000000000000000000000000000000 file:/C/Folder/MyCoolApp-0.0.nupkg 0")] + [InlineData(@"0000000000000000000000000000000000000000 C:\Folder\MyCoolApp-0.0.nupkg 0")] + [InlineData(@"0000000000000000000000000000000000000000 ..\OtherFolder\MyCoolApp-0.0.nupkg 0")] + [InlineData(@"0000000000000000000000000000000000000000 ../OtherFolder/MyCoolApp-0.0.nupkg 0")] + [InlineData(@"0000000000000000000000000000000000000000 \\Somewhere\NetworkShare\MyCoolApp-0.0.nupkg.delta 0")] + public void ParseThrowsWhenInvalidReleaseEntryLines(string releaseEntry) + { + Assert.Throws(() => ReleaseEntry.ParseReleaseEntry(releaseEntry)); + } + + [Theory] + [InlineData(@"0000000000000000000000000000000000000000 file.nupkg 0")] + [InlineData(@"0000000000000000000000000000000000000000 http://path/file.nupkg 0")] + public void EntryAsStringMatchesParsedInput(string releaseEntry) + { + var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); + Assert.Equal(releaseEntry, fixture.EntryAsString); + } + + [Theory] + [InlineData("Squirrel.Core.1.0.0.0.nupkg", 4457, "75255cfd229a1ed1447abe1104f5635e69975d30")] + [InlineData("Squirrel.Core.1.1.0.0.nupkg", 15830, "9baf1dbacb09940086c8c62d9a9dbe69fe1f7593")] + public void GenerateFromFileTest(string name, long size, string sha1) + { + var path = PathHelper.GetFixture(name); + + using (var f = File.OpenRead(path)) { + var fixture = ReleaseEntry.GenerateFromFile(f, "dontcare"); + Assert.Equal(size, fixture.Filesize); + Assert.Equal(sha1, fixture.SHA1.ToLowerInvariant()); + } + } + + [Theory] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.nupkg 123", 1, 2, 0, 0, "", false)] + [InlineData("1000000000000000000000000000000000000000 MyCoolApp-1.2-full.nupkg 123", 1, 2, 0, 0, "", false)] + [InlineData("2000000000000000000000000000000000000000 MyCoolApp-1.2-delta.nupkg 123", 1, 2, 0, 0, "", true)] + [InlineData("3000000000000000000000000000000000000000 MyCoolApp-1.2-beta1.nupkg 123", 1, 2, 0, 0, "beta1", false)] + [InlineData("4000000000000000000000000000000000000000 MyCoolApp-1.2-beta1-full.nupkg 123", 1, 2, 0, 0, "beta1", false)] + [InlineData("5000000000000000000000000000000000000000 MyCoolApp-1.2-beta1-delta.nupkg 123", 1, 2, 0, 0, "beta1", true)] + [InlineData("6000000000000000000000000000000000000000 MyCoolApp-1.2.3.nupkg 123", 1, 2, 3, 0, "", false)] + [InlineData("7000000000000000000000000000000000000000 MyCoolApp-1.2.3-full.nupkg 123", 1, 2, 3, 0, "", false)] + [InlineData("8000000000000000000000000000000000000000 MyCoolApp-1.2.3-delta.nupkg 123", 1, 2, 3, 0, "", true)] + [InlineData("9000000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1.nupkg 123", 1, 2, 3, 0, "beta1", false)] + [InlineData("0100000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1-full.nupkg 123", 1, 2, 3, 0, "beta1", false)] + [InlineData("0200000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1-delta.nupkg 123", 1, 2, 3, 0, "beta1", true)] + [InlineData("0300000000000000000000000000000000000000 MyCoolApp-1.2.3.4.nupkg 123", 1, 2, 3, 4, "", false)] + [InlineData("0400000000000000000000000000000000000000 MyCoolApp-1.2.3.4-full.nupkg 123", 1, 2, 3, 4, "", false)] + [InlineData("0500000000000000000000000000000000000000 MyCoolApp-1.2.3.4-delta.nupkg 123", 1, 2, 3, 4, "", true)] + [InlineData("0600000000000000000000000000000000000000 MyCoolApp-1.2.3.4-beta1.nupkg 123", 1, 2, 3, 4, "beta1", false)] + [InlineData("0700000000000000000000000000000000000000 MyCoolApp-1.2.3.4-beta1-full.nupkg 123", 1, 2, 3, 4, "beta1", false)] + [InlineData("0800000000000000000000000000000000000000 MyCoolApp-1.2.3.4-beta1-delta.nupkg 123", 1, 2, 3, 4, "beta1", true)] + + public void ParseVersionTest(string releaseEntry, int major, int minor, int patch, int revision, string prerelease, bool isDelta) + { + var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); + + Assert.Equal(new NuGetVersion(major, minor, patch, revision, prerelease, null), fixture.Version); + Assert.Equal(isDelta, fixture.IsDelta); + + var old = OldReleaseEntry.ParseReleaseEntry(releaseEntry); + Assert.Equal(new NuGetVersion(major, minor, patch, revision, prerelease, null), new NuGetVersion(old.Version.ToString())); + Assert.Equal(isDelta, old.IsDelta); + } + + [Theory] + [InlineData("0000000000000000000000000000000000000000 MyCool-App-1.2.nupkg 123", "MyCool-App")] + [InlineData("0000000000000000000000000000000000000000 MyCool_App-1.2-full.nupkg 123", "MyCool_App")] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-delta.nupkg 123", "MyCoolApp")] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-beta1.nupkg 123", "MyCoolApp")] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-beta1-full.nupkg 123", "MyCoolApp")] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-beta1-delta.nupkg 123", "MyCoolApp")] + [InlineData("0000000000000000000000000000000000000000 MyCool-App-1.2.3.nupkg 123", "MyCool-App")] + [InlineData("0000000000000000000000000000000000000000 MyCool_App-1.2.3-full.nupkg 123", "MyCool_App")] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3-delta.nupkg 123", "MyCoolApp")] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1.nupkg 123", "MyCoolApp")] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1-full.nupkg 123", "MyCoolApp")] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1-delta.nupkg 123", "MyCoolApp")] + [InlineData("0000000000000000000000000000000000000000 MyCool-App-1.2.3.4.nupkg 123", "MyCool-App")] + [InlineData("0000000000000000000000000000000000000000 MyCool_App-1.2.3.4-full.nupkg 123", "MyCool_App")] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3.4-delta.nupkg 123", "MyCoolApp")] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3.4-beta1.nupkg 123", "MyCoolApp")] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3.4-beta1-full.nupkg 123", "MyCoolApp")] + [InlineData("0000000000000000000000000000000000000000 MyCool-App-1.2.3.4-beta1-delta.nupkg 123", "MyCool-App")] + public void CheckPackageName(string releaseEntry, string expected) + { + var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); + Assert.Equal(expected, fixture.PackageId); + + var old = OldReleaseEntry.ParseReleaseEntry(releaseEntry); + Assert.Equal(expected, old.PackageName); + } + + [Theory] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.nupkg 123 # 10%", 1, 2, 0, 0, "", "", false, 0.1f)] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-full.nupkg 123 # 90%", 1, 2, 0, 0, "", "", false, 0.9f)] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-delta.nupkg 123", 1, 2, 0, 0, "", "", true, null)] + [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-delta.nupkg 123 # 5%", 1, 2, 0, 0, "", "", true, 0.05f)] + public void ParseStagingPercentageTest(string releaseEntry, int major, int minor, int patch, int revision, string prerelease, string rid, bool isDelta, float? stagingPercentage) + { + var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); + + Assert.Equal(new NuGetVersion(major, minor, patch, revision, prerelease, null), fixture.Version); + Assert.Equal(isDelta, fixture.IsDelta); + + if (stagingPercentage.HasValue) { + Assert.True(Math.Abs(fixture.StagingPercentage.Value - stagingPercentage.Value) < 0.001); + } else { + Assert.Null(fixture.StagingPercentage); } - [Theory] - [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec MyCoolApp-1.0.nupkg 1004502", "MyCoolApp-1.0.nupkg", 1004502, null, null)] - [InlineData(@"3a2eadd15dd984e4559f2b4d790ec8badaeb6a39 MyCoolApp-1.1.nupkg 1040561", "MyCoolApp-1.1.nupkg", 1040561, null, null)] - [InlineData(@"14db31d2647c6d2284882a2e101924a9c409ee67 MyCoolApp-1.1.nupkg.delta 80396", "MyCoolApp-1.1.nupkg.delta", 80396, null, null)] - [InlineData(@"0000000000000000000000000000000000000000 http://test.org/Folder/MyCoolApp-1.2.nupkg 2569", "MyCoolApp-1.2.nupkg", 2569, "http://test.org/Folder/", null)] - [InlineData(@"0000000000000000000000000000000000000000 http://test.org/Folder/MyCoolApp-1.2.nupkg?query=param 2569", "MyCoolApp-1.2.nupkg", 2569, "http://test.org/Folder/", "?query=param")] - [InlineData(@"0000000000000000000000000000000000000000 https://www.test.org/Folder/MyCoolApp-1.2-delta.nupkg 1231953", "MyCoolApp-1.2-delta.nupkg", 1231953, "https://www.test.org/Folder/", null)] - [InlineData(@"0000000000000000000000000000000000000000 https://www.test.org/Folder/MyCoolApp-1.2-delta.nupkg?query=param 1231953", "MyCoolApp-1.2-delta.nupkg", 1231953, "https://www.test.org/Folder/", "?query=param")] - public void ParseValidReleaseEntryLines(string releaseEntry, string fileName, long fileSize, string baseUrl, string query) - { - var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); + var old = OldReleaseEntry.ParseReleaseEntry(releaseEntry); + var legacyPre = !String.IsNullOrEmpty(prerelease) && !String.IsNullOrEmpty(rid) ? $"{prerelease}-{rid}" : String.IsNullOrEmpty(prerelease) ? rid : prerelease; + Assert.Equal(new NuGetVersion(major, minor, patch, revision, legacyPre, null), new NuGetVersion(old.Version.ToString())); + Assert.Equal(isDelta, old.IsDelta); - Assert.Equal(fileName, fixture.OriginalFilename); - Assert.Equal(fileSize, fixture.Filesize); - Assert.Equal(baseUrl, fixture.BaseUrl); - Assert.Equal(query, fixture.Query); - - var old = OldReleaseEntry.ParseReleaseEntry(releaseEntry); - Assert.Equal(fileName, old.Filename); - Assert.Equal(fileSize, old.Filesize); - Assert.Equal(baseUrl, old.BaseUrl); - Assert.Equal(query, old.Query); + if (stagingPercentage.HasValue) { + Assert.True(Math.Abs(old.StagingPercentage.Value - stagingPercentage.Value) < 0.001); + } else { + Assert.Null(old.StagingPercentage); } - - [Theory] - [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec My.Cool.App-1.0-full.nupkg 1004502", "My.Cool.App")] - [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec My.Cool.App-1.1.nupkg 1004502", "My.Cool.App")] - [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec http://test.org/Folder/My.Cool.App-1.2.nupkg?query=param 1231953", "My.Cool.App")] - public void ParseValidReleaseEntryLinesWithDots(string releaseEntry, string packageName) - { - var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); - Assert.Equal(packageName, fixture.PackageId); - } - - [Theory] - [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec My-Cool-App-1.0-full.nupkg 1004502", "My-Cool-App")] - [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec My.Cool-App-1.1.nupkg 1004502", "My.Cool-App")] - [InlineData(@"94689fede03fed7ab59c24337673a27837f0c3ec http://test.org/Folder/My.Cool-App-1.2.nupkg?query=param 1231953", "My.Cool-App")] - public void ParseValidReleaseEntryLinesWithDashes(string releaseEntry, string packageName) - { - var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); - Assert.Equal(packageName, fixture.PackageId); - } - - [Theory] - [InlineData(@"0000000000000000000000000000000000000000 file:/C/Folder/MyCoolApp-0.0.nupkg 0")] - [InlineData(@"0000000000000000000000000000000000000000 C:\Folder\MyCoolApp-0.0.nupkg 0")] - [InlineData(@"0000000000000000000000000000000000000000 ..\OtherFolder\MyCoolApp-0.0.nupkg 0")] - [InlineData(@"0000000000000000000000000000000000000000 ../OtherFolder/MyCoolApp-0.0.nupkg 0")] - [InlineData(@"0000000000000000000000000000000000000000 \\Somewhere\NetworkShare\MyCoolApp-0.0.nupkg.delta 0")] - public void ParseThrowsWhenInvalidReleaseEntryLines(string releaseEntry) - { - Assert.Throws(() => ReleaseEntry.ParseReleaseEntry(releaseEntry)); - } - - [Theory] - [InlineData(@"0000000000000000000000000000000000000000 file.nupkg 0")] - [InlineData(@"0000000000000000000000000000000000000000 http://path/file.nupkg 0")] - public void EntryAsStringMatchesParsedInput(string releaseEntry) - { - var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); - Assert.Equal(releaseEntry, fixture.EntryAsString); - } - - [Theory] - [InlineData("Squirrel.Core.1.0.0.0.nupkg", 4457, "75255cfd229a1ed1447abe1104f5635e69975d30")] - [InlineData("Squirrel.Core.1.1.0.0.nupkg", 15830, "9baf1dbacb09940086c8c62d9a9dbe69fe1f7593")] - public void GenerateFromFileTest(string name, long size, string sha1) - { - var path = PathHelper.GetFixture(name); - - using (var f = File.OpenRead(path)) { - var fixture = ReleaseEntry.GenerateFromFile(f, "dontcare"); - Assert.Equal(size, fixture.Filesize); - Assert.Equal(sha1, fixture.SHA1.ToLowerInvariant()); - } - } - - [Theory] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.nupkg 123", 1, 2, 0, 0, "", false)] - [InlineData("1000000000000000000000000000000000000000 MyCoolApp-1.2-full.nupkg 123", 1, 2, 0, 0, "", false)] - [InlineData("2000000000000000000000000000000000000000 MyCoolApp-1.2-delta.nupkg 123", 1, 2, 0, 0, "", true)] - [InlineData("3000000000000000000000000000000000000000 MyCoolApp-1.2-beta1.nupkg 123", 1, 2, 0, 0, "beta1", false)] - [InlineData("4000000000000000000000000000000000000000 MyCoolApp-1.2-beta1-full.nupkg 123", 1, 2, 0, 0, "beta1", false)] - [InlineData("5000000000000000000000000000000000000000 MyCoolApp-1.2-beta1-delta.nupkg 123", 1, 2, 0, 0, "beta1", true)] - [InlineData("6000000000000000000000000000000000000000 MyCoolApp-1.2.3.nupkg 123", 1, 2, 3, 0, "", false)] - [InlineData("7000000000000000000000000000000000000000 MyCoolApp-1.2.3-full.nupkg 123", 1, 2, 3, 0, "", false)] - [InlineData("8000000000000000000000000000000000000000 MyCoolApp-1.2.3-delta.nupkg 123", 1, 2, 3, 0, "", true)] - [InlineData("9000000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1.nupkg 123", 1, 2, 3, 0, "beta1", false)] - [InlineData("0100000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1-full.nupkg 123", 1, 2, 3, 0, "beta1", false)] - [InlineData("0200000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1-delta.nupkg 123", 1, 2, 3, 0, "beta1", true)] - [InlineData("0300000000000000000000000000000000000000 MyCoolApp-1.2.3.4.nupkg 123", 1, 2, 3, 4, "", false)] - [InlineData("0400000000000000000000000000000000000000 MyCoolApp-1.2.3.4-full.nupkg 123", 1, 2, 3, 4, "", false)] - [InlineData("0500000000000000000000000000000000000000 MyCoolApp-1.2.3.4-delta.nupkg 123", 1, 2, 3, 4, "", true)] - [InlineData("0600000000000000000000000000000000000000 MyCoolApp-1.2.3.4-beta1.nupkg 123", 1, 2, 3, 4, "beta1", false)] - [InlineData("0700000000000000000000000000000000000000 MyCoolApp-1.2.3.4-beta1-full.nupkg 123", 1, 2, 3, 4, "beta1", false)] - [InlineData("0800000000000000000000000000000000000000 MyCoolApp-1.2.3.4-beta1-delta.nupkg 123", 1, 2, 3, 4, "beta1", true)] - - public void ParseVersionTest(string releaseEntry, int major, int minor, int patch, int revision, string prerelease, bool isDelta) - { - var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); - - Assert.Equal(new NuGetVersion(major, minor, patch, revision, prerelease, null), fixture.Version); - Assert.Equal(isDelta, fixture.IsDelta); - - var old = OldReleaseEntry.ParseReleaseEntry(releaseEntry); - Assert.Equal(new NuGetVersion(major, minor, patch, revision, prerelease, null), new NuGetVersion(old.Version.ToString())); - Assert.Equal(isDelta, old.IsDelta); - } - - [Theory] - [InlineData("0000000000000000000000000000000000000000 MyCool-App-1.2.nupkg 123", "MyCool-App")] - [InlineData("0000000000000000000000000000000000000000 MyCool_App-1.2-full.nupkg 123", "MyCool_App")] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-delta.nupkg 123", "MyCoolApp")] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-beta1.nupkg 123", "MyCoolApp")] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-beta1-full.nupkg 123", "MyCoolApp")] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-beta1-delta.nupkg 123", "MyCoolApp")] - [InlineData("0000000000000000000000000000000000000000 MyCool-App-1.2.3.nupkg 123", "MyCool-App")] - [InlineData("0000000000000000000000000000000000000000 MyCool_App-1.2.3-full.nupkg 123", "MyCool_App")] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3-delta.nupkg 123", "MyCoolApp")] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1.nupkg 123", "MyCoolApp")] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1-full.nupkg 123", "MyCoolApp")] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3-beta1-delta.nupkg 123", "MyCoolApp")] - [InlineData("0000000000000000000000000000000000000000 MyCool-App-1.2.3.4.nupkg 123", "MyCool-App")] - [InlineData("0000000000000000000000000000000000000000 MyCool_App-1.2.3.4-full.nupkg 123", "MyCool_App")] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3.4-delta.nupkg 123", "MyCoolApp")] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3.4-beta1.nupkg 123", "MyCoolApp")] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.3.4-beta1-full.nupkg 123", "MyCoolApp")] - [InlineData("0000000000000000000000000000000000000000 MyCool-App-1.2.3.4-beta1-delta.nupkg 123", "MyCool-App")] - public void CheckPackageName(string releaseEntry, string expected) - { - var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); - Assert.Equal(expected, fixture.PackageId); - - var old = OldReleaseEntry.ParseReleaseEntry(releaseEntry); - Assert.Equal(expected, old.PackageName); - } - - [Theory] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2.nupkg 123 # 10%", 1, 2, 0, 0, "", "", false, 0.1f)] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-full.nupkg 123 # 90%", 1, 2, 0, 0, "", "", false, 0.9f)] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-delta.nupkg 123", 1, 2, 0, 0, "", "", true, null)] - [InlineData("0000000000000000000000000000000000000000 MyCoolApp-1.2-delta.nupkg 123 # 5%", 1, 2, 0, 0, "", "", true, 0.05f)] - public void ParseStagingPercentageTest(string releaseEntry, int major, int minor, int patch, int revision, string prerelease, string rid, bool isDelta, float? stagingPercentage) - { - var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry); - - Assert.Equal(new NuGetVersion(major, minor, patch, revision, prerelease, null), fixture.Version); - Assert.Equal(isDelta, fixture.IsDelta); - - if (stagingPercentage.HasValue) { - Assert.True(Math.Abs(fixture.StagingPercentage.Value - stagingPercentage.Value) < 0.001); - } else { - Assert.Null(fixture.StagingPercentage); - } - - var old = OldReleaseEntry.ParseReleaseEntry(releaseEntry); - var legacyPre = !String.IsNullOrEmpty(prerelease) && !String.IsNullOrEmpty(rid) ? $"{prerelease}-{rid}" : String.IsNullOrEmpty(prerelease) ? rid : prerelease; - Assert.Equal(new NuGetVersion(major, minor, patch, revision, legacyPre, null), new NuGetVersion(old.Version.ToString())); - Assert.Equal(isDelta, old.IsDelta); - - if (stagingPercentage.HasValue) { - Assert.True(Math.Abs(old.StagingPercentage.Value - stagingPercentage.Value) < 0.001); - } else { - Assert.Null(old.StagingPercentage); - } - } - - [Fact] - public void CanParseGeneratedReleaseEntryAsString() - { - var path = PathHelper.GetFixture("Squirrel.Core.1.1.0.0.nupkg"); - var entryAsString = ReleaseEntry.GenerateFromFile(path).EntryAsString; - ReleaseEntry.ParseReleaseEntry(entryAsString); - } - - //[Fact] - //public void GetLatestReleaseWithNullCollectionReturnsNull() - //{ - // Assert.Null(ReleasePackageBuilder.GetPreviousRelease( - // null, null, null, null)); - //} - - //[Fact] - //public void GetLatestReleaseWithEmptyCollectionReturnsNull() - //{ - // Assert.Null(ReleasePackageBuilder.GetPreviousRelease( - // Enumerable.Empty(), null, null, null)); - //} - - //[Fact] - //public void WhenCurrentReleaseMatchesLastReleaseReturnNull() - //{ - // var package = new ReleasePackageBuilder("Espera-1.7.6-beta.nupkg"); - - // var releaseEntries = new[] { - // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg")) - // }; - // Assert.Null(ReleasePackageBuilder.GetPreviousRelease( - // releaseEntries, package, @"C:\temp\somefolder", null)); - //} - - //[Fact] - //public void WhenMultipleReleaseMatchesReturnEarlierResult() - //{ - // var expected = SemanticVersion.Parse("1.7.5-beta"); - // var package = new ReleasePackageBuilder("Espera-1.7.6-beta.nupkg"); - - // var releaseEntries = new[] { - // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg")), - // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.5-beta.nupkg")) - // }; - - // var actual = ReleasePackageBuilder.GetPreviousRelease( - // releaseEntries, - // package, - // @"C:\temp\", null); - - // Assert.Equal(expected, actual.Version); - //} - - //[Fact] - //public void WhenMultipleReleasesFoundReturnPreviousVersion() - //{ - // var expected = SemanticVersion.Parse("1.7.6-beta"); - // var input = new ReleasePackageBuilder("Espera-1.7.7-beta.nupkg"); - - // var releaseEntries = new[] { - // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg")), - // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.5-beta.nupkg")) - // }; - - // var actual = ReleasePackageBuilder.GetPreviousRelease( - // releaseEntries, - // input, - // @"C:\temp\", null); - - // Assert.Equal(expected, actual.Version); - //} - - //[Fact] - //public void WhenMultipleReleasesFoundInOtherOrderReturnPreviousVersion() - //{ - // var expected = SemanticVersion.Parse("1.7.6-beta"); - // var input = new ReleasePackageBuilder("Espera-1.7.7-beta.nupkg"); - - // var releaseEntries = new[] { - // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.5-beta.nupkg")), - // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg")) - // }; - - // var actual = ReleasePackageBuilder.GetPreviousRelease( - // releaseEntries, - // input, - // @"C:\temp\", null); - - // Assert.Equal(expected, actual.Version); - //} - - [Fact] - public void WhenReleasesAreOutOfOrderSortByVersion() - { - var path = Path.GetTempFileName(); - var firstVersion = SemanticVersion.Parse("1.0.0"); - var secondVersion = SemanticVersion.Parse("1.1.0"); - var thirdVersion = SemanticVersion.Parse("1.2.0"); - - var releaseEntries = new[] { - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-delta.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-delta.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) - }; - - ReleaseEntry.WriteReleaseFile(releaseEntries, path); - - var releases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(path)).ToArray(); - - Assert.Equal(firstVersion, releases[0].Version); - Assert.Equal(secondVersion, releases[1].Version); - Assert.Equal(true, releases[1].IsDelta); - Assert.Equal(secondVersion, releases[2].Version); - Assert.Equal(false, releases[2].IsDelta); - Assert.Equal(thirdVersion, releases[3].Version); - Assert.Equal(true, releases[3].IsDelta); - Assert.Equal(thirdVersion, releases[4].Version); - Assert.Equal(false, releases[4].IsDelta); - } - - [Fact] - public void WhenPreReleasesAreOutOfOrderSortByNumericSuffixSemVer2() - { - var path = Path.GetTempFileName(); - var firstVersion = SemanticVersion.Parse("1.1.9-beta.105"); - var secondVersion = SemanticVersion.Parse("1.2.0-beta.9"); - var thirdVersion = SemanticVersion.Parse("1.2.0-beta.10"); - var fourthVersion = SemanticVersion.Parse("1.2.0-beta.100"); - - var releaseEntries = new[] { - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-beta.1-full.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-beta.9-full.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-beta.100-full.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.9-beta.105-full.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-beta.10-full.nupkg")) - }; - - ReleaseEntry.WriteReleaseFile(releaseEntries, path); - - var releases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(path)).ToArray(); - - Assert.Equal(firstVersion, releases[0].Version); - Assert.Equal(secondVersion, releases[2].Version); - Assert.Equal(thirdVersion, releases[3].Version); - Assert.Equal(fourthVersion, releases[4].Version); - } - - [Fact] - public void StagingUsersGetBetaSoftware() - { - // NB: We're kind of using a hack here, in that we know that the - // last 4 bytes are used as the percentage, and the percentage - // effectively measures, "How close are you to zero". Guid.Empty - // is v close to zero, because it is zero. - var path = Path.GetTempFileName(); - var ourGuid = Guid.Empty; - - var releaseEntries = new[] { - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.1f)), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) - }; - - ReleaseEntry.WriteReleaseFile(releaseEntries, path); - - var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); - Assert.Equal(3, releases.Length); - } - - [Fact] - public void BorkedUsersGetProductionSoftware() - { - var path = Path.GetTempFileName(); - var ourGuid = default(Guid?); - - var releaseEntries = new[] { - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.1f)), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) - }; - - ReleaseEntry.WriteReleaseFile(releaseEntries, path); - - var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); - Assert.Equal(2, releases.Length); - } - - [Theory] - [InlineData("{22b29e6f-bd2e-43d2-85ca-ffffffffffff}")] - [InlineData("{22b29e6f-bd2e-43d2-85ca-888888888888}")] - [InlineData("{22b29e6f-bd2e-43d2-85ca-444444444444}")] - public void UnluckyUsersGetProductionSoftware(string inputGuid) - { - var path = Path.GetTempFileName(); - var ourGuid = Guid.ParseExact(inputGuid, "B"); - - var releaseEntries = new[] { - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.1f)), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) - }; - - ReleaseEntry.WriteReleaseFile(releaseEntries, path); - - var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); - Assert.Equal(2, releases.Length); - } - - [Theory] - [InlineData("{22b29e6f-bd2e-43d2-85ca-333333333333}")] - [InlineData("{22b29e6f-bd2e-43d2-85ca-111111111111}")] - [InlineData("{22b29e6f-bd2e-43d2-85ca-000000000000}")] - public void LuckyUsersGetBetaSoftware(string inputGuid) - { - var path = Path.GetTempFileName(); - var ourGuid = Guid.ParseExact(inputGuid, "B"); - - var releaseEntries = new[] { - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.25f)), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), - ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) - }; - - ReleaseEntry.WriteReleaseFile(releaseEntries, path); - - var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); - Assert.Equal(3, releases.Length); - } - - [Fact] - public void ParseReleaseFileShouldReturnNothingForBlankFiles() - { - Assert.True(ReleaseEntry.ParseReleaseFile("").Count() == 0); - Assert.True(ReleaseEntry.ParseReleaseFile(null).Count() == 0); - } - - // [Fact] - // public void FindCurrentVersionWithExactRidMatch() - // { - // string _ridReleaseEntries = """ - //0000000000000000000000000000000000000000 MyApp-1.3-win-x86.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4-win-x64.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4-win-x86.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4-osx-x86.nupkg 123 - //"""; - - // var entries = ReleaseEntry.ParseReleaseFile(_ridReleaseEntries); - - // var e = Utility.FindLatestFullVersion(entries, RID.Parse("win-x86")); - // Assert.Equal("MyApp-1.4-win-x86.nupkg", e.OriginalFilename); - // } - - // [Fact] - // public void FindCurrentVersionWithExactRidMatchNotLatest() - // { - // string _ridReleaseEntries = """ - //0000000000000000000000000000000000000000 MyApp-1.3-win-x86.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4-win-x64.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4-win.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4-osx-x86.nupkg 123 - //"""; - - // var entries = ReleaseEntry.ParseReleaseFile(_ridReleaseEntries); - - // var e = Utility.FindLatestFullVersion(entries, RID.Parse("win-x86")); - // Assert.Equal("MyApp-1.3-win.nupkg", e.OriginalFilename); - // } - - // [Fact] - // public void FindCurrentVersionWithExactRidMatchOnlyArchitecture() - // { - // string _ridReleaseEntries = """ - //0000000000000000000000000000000000000000 MyApp-1.3-win-x86.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4-win-x64.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4-win.nupkg 123 - //0000000000000000000000000000000000000000 MyApp-1.4-osx-x86.nupkg 123 - //"""; - - // var entries = ReleaseEntry.ParseReleaseFile(_ridReleaseEntries); - - // var e = Utility.FindLatestFullVersion(entries, RID.Parse("win-x86")); - // Assert.Equal("MyApp-1.3-win.nupkg", e.OriginalFilename); - // } - - static string MockReleaseEntry(string name, float? percentage = null) - { - if (percentage.HasValue) { - var ret = String.Format("94689fede03fed7ab59c24337673a27837f0c3ec {0} 1004502 # {1:F0}%", name, percentage * 100.0f); - return ret; - } else { - return String.Format("94689fede03fed7ab59c24337673a27837f0c3ec {0} 1004502", name); - } + } + + [Fact] + public void CanParseGeneratedReleaseEntryAsString() + { + var path = PathHelper.GetFixture("Squirrel.Core.1.1.0.0.nupkg"); + var entryAsString = ReleaseEntry.GenerateFromFile(path).EntryAsString; + ReleaseEntry.ParseReleaseEntry(entryAsString); + } + + //[Fact] + //public void GetLatestReleaseWithNullCollectionReturnsNull() + //{ + // Assert.Null(ReleasePackageBuilder.GetPreviousRelease( + // null, null, null, null)); + //} + + //[Fact] + //public void GetLatestReleaseWithEmptyCollectionReturnsNull() + //{ + // Assert.Null(ReleasePackageBuilder.GetPreviousRelease( + // Enumerable.Empty(), null, null, null)); + //} + + //[Fact] + //public void WhenCurrentReleaseMatchesLastReleaseReturnNull() + //{ + // var package = new ReleasePackageBuilder("Espera-1.7.6-beta.nupkg"); + + // var releaseEntries = new[] { + // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg")) + // }; + // Assert.Null(ReleasePackageBuilder.GetPreviousRelease( + // releaseEntries, package, @"C:\temp\somefolder", null)); + //} + + //[Fact] + //public void WhenMultipleReleaseMatchesReturnEarlierResult() + //{ + // var expected = SemanticVersion.Parse("1.7.5-beta"); + // var package = new ReleasePackageBuilder("Espera-1.7.6-beta.nupkg"); + + // var releaseEntries = new[] { + // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg")), + // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.5-beta.nupkg")) + // }; + + // var actual = ReleasePackageBuilder.GetPreviousRelease( + // releaseEntries, + // package, + // @"C:\temp\", null); + + // Assert.Equal(expected, actual.Version); + //} + + //[Fact] + //public void WhenMultipleReleasesFoundReturnPreviousVersion() + //{ + // var expected = SemanticVersion.Parse("1.7.6-beta"); + // var input = new ReleasePackageBuilder("Espera-1.7.7-beta.nupkg"); + + // var releaseEntries = new[] { + // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg")), + // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.5-beta.nupkg")) + // }; + + // var actual = ReleasePackageBuilder.GetPreviousRelease( + // releaseEntries, + // input, + // @"C:\temp\", null); + + // Assert.Equal(expected, actual.Version); + //} + + //[Fact] + //public void WhenMultipleReleasesFoundInOtherOrderReturnPreviousVersion() + //{ + // var expected = SemanticVersion.Parse("1.7.6-beta"); + // var input = new ReleasePackageBuilder("Espera-1.7.7-beta.nupkg"); + + // var releaseEntries = new[] { + // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.5-beta.nupkg")), + // ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg")) + // }; + + // var actual = ReleasePackageBuilder.GetPreviousRelease( + // releaseEntries, + // input, + // @"C:\temp\", null); + + // Assert.Equal(expected, actual.Version); + //} + + [Fact] + public void WhenReleasesAreOutOfOrderSortByVersion() + { + var path = Path.GetTempFileName(); + var firstVersion = SemanticVersion.Parse("1.0.0"); + var secondVersion = SemanticVersion.Parse("1.1.0"); + var thirdVersion = SemanticVersion.Parse("1.2.0"); + + var releaseEntries = new[] { + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-delta.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-delta.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) + }; + + ReleaseEntry.WriteReleaseFile(releaseEntries, path); + + var releases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(path)).ToArray(); + + Assert.Equal(firstVersion, releases[0].Version); + Assert.Equal(secondVersion, releases[1].Version); + Assert.Equal(true, releases[1].IsDelta); + Assert.Equal(secondVersion, releases[2].Version); + Assert.Equal(false, releases[2].IsDelta); + Assert.Equal(thirdVersion, releases[3].Version); + Assert.Equal(true, releases[3].IsDelta); + Assert.Equal(thirdVersion, releases[4].Version); + Assert.Equal(false, releases[4].IsDelta); + } + + [Fact] + public void WhenPreReleasesAreOutOfOrderSortByNumericSuffixSemVer2() + { + var path = Path.GetTempFileName(); + var firstVersion = SemanticVersion.Parse("1.1.9-beta.105"); + var secondVersion = SemanticVersion.Parse("1.2.0-beta.9"); + var thirdVersion = SemanticVersion.Parse("1.2.0-beta.10"); + var fourthVersion = SemanticVersion.Parse("1.2.0-beta.100"); + + var releaseEntries = new[] { + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-beta.1-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-beta.9-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-beta.100-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.9-beta.105-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-beta.10-full.nupkg")) + }; + + ReleaseEntry.WriteReleaseFile(releaseEntries, path); + + var releases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(path)).ToArray(); + + Assert.Equal(firstVersion, releases[0].Version); + Assert.Equal(secondVersion, releases[2].Version); + Assert.Equal(thirdVersion, releases[3].Version); + Assert.Equal(fourthVersion, releases[4].Version); + } + + [Fact] + public void StagingUsersGetBetaSoftware() + { + // NB: We're kind of using a hack here, in that we know that the + // last 4 bytes are used as the percentage, and the percentage + // effectively measures, "How close are you to zero". Guid.Empty + // is v close to zero, because it is zero. + var path = Path.GetTempFileName(); + var ourGuid = Guid.Empty; + + var releaseEntries = new[] { + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.1f)), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) + }; + + ReleaseEntry.WriteReleaseFile(releaseEntries, path); + + var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); + Assert.Equal(3, releases.Length); + } + + [Fact] + public void BorkedUsersGetProductionSoftware() + { + var path = Path.GetTempFileName(); + var ourGuid = default(Guid?); + + var releaseEntries = new[] { + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.1f)), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) + }; + + ReleaseEntry.WriteReleaseFile(releaseEntries, path); + + var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); + Assert.Equal(2, releases.Length); + } + + [Theory] + [InlineData("{22b29e6f-bd2e-43d2-85ca-ffffffffffff}")] + [InlineData("{22b29e6f-bd2e-43d2-85ca-888888888888}")] + [InlineData("{22b29e6f-bd2e-43d2-85ca-444444444444}")] + public void UnluckyUsersGetProductionSoftware(string inputGuid) + { + var path = Path.GetTempFileName(); + var ourGuid = Guid.ParseExact(inputGuid, "B"); + + var releaseEntries = new[] { + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.1f)), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) + }; + + ReleaseEntry.WriteReleaseFile(releaseEntries, path); + + var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); + Assert.Equal(2, releases.Length); + } + + [Theory] + [InlineData("{22b29e6f-bd2e-43d2-85ca-333333333333}")] + [InlineData("{22b29e6f-bd2e-43d2-85ca-111111111111}")] + [InlineData("{22b29e6f-bd2e-43d2-85ca-000000000000}")] + public void LuckyUsersGetBetaSoftware(string inputGuid) + { + var path = Path.GetTempFileName(); + var ourGuid = Guid.ParseExact(inputGuid, "B"); + + var releaseEntries = new[] { + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg", 0.25f)), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")), + ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg")) + }; + + ReleaseEntry.WriteReleaseFile(releaseEntries, path); + + var releases = ReleaseEntry.ParseReleaseFileAndApplyStaging(File.ReadAllText(path), ourGuid).ToArray(); + Assert.Equal(3, releases.Length); + } + + [Fact] + public void ParseReleaseFileShouldReturnNothingForBlankFiles() + { + Assert.True(ReleaseEntry.ParseReleaseFile("").Count() == 0); + Assert.True(ReleaseEntry.ParseReleaseFile(null).Count() == 0); + } + + // [Fact] + // public void FindCurrentVersionWithExactRidMatch() + // { + // string _ridReleaseEntries = """ + //0000000000000000000000000000000000000000 MyApp-1.3-win-x86.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4-win-x64.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4-win-x86.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4-osx-x86.nupkg 123 + //"""; + + // var entries = ReleaseEntry.ParseReleaseFile(_ridReleaseEntries); + + // var e = Utility.FindLatestFullVersion(entries, RID.Parse("win-x86")); + // Assert.Equal("MyApp-1.4-win-x86.nupkg", e.OriginalFilename); + // } + + // [Fact] + // public void FindCurrentVersionWithExactRidMatchNotLatest() + // { + // string _ridReleaseEntries = """ + //0000000000000000000000000000000000000000 MyApp-1.3-win-x86.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4-win-x64.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4-win.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4-osx-x86.nupkg 123 + //"""; + + // var entries = ReleaseEntry.ParseReleaseFile(_ridReleaseEntries); + + // var e = Utility.FindLatestFullVersion(entries, RID.Parse("win-x86")); + // Assert.Equal("MyApp-1.3-win.nupkg", e.OriginalFilename); + // } + + // [Fact] + // public void FindCurrentVersionWithExactRidMatchOnlyArchitecture() + // { + // string _ridReleaseEntries = """ + //0000000000000000000000000000000000000000 MyApp-1.3-win-x86.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4-win-x64.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4-win.nupkg 123 + //0000000000000000000000000000000000000000 MyApp-1.4-osx-x86.nupkg 123 + //"""; + + // var entries = ReleaseEntry.ParseReleaseFile(_ridReleaseEntries); + + // var e = Utility.FindLatestFullVersion(entries, RID.Parse("win-x86")); + // Assert.Equal("MyApp-1.3-win.nupkg", e.OriginalFilename); + // } + + static string MockReleaseEntry(string name, float? percentage = null) + { + if (percentage.HasValue) { + var ret = String.Format("94689fede03fed7ab59c24337673a27837f0c3ec {0} 1004502 # {1:F0}%", name, percentage * 100.0f); + return ret; + } else { + return String.Format("94689fede03fed7ab59c24337673a27837f0c3ec {0} 1004502", name); } } } diff --git a/test/Velopack.Tests/RuntimeInfoTests.cs b/test/Velopack.Tests/RuntimeInfoTests.cs index 4f3af06f..37591a0a 100644 --- a/test/Velopack.Tests/RuntimeInfoTests.cs +++ b/test/Velopack.Tests/RuntimeInfoTests.cs @@ -6,40 +6,39 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -namespace Velopack.Tests -{ - public class RuntimeInfoTests - { - [Fact] - public void NugetVersionAgreesWithNbgv() - { - var args = new List { "get-version", "-v", "NuGetPackageVersion" }; - var psi = new ProcessStartInfo("nbgv"); - psi.AppendArgumentListSafe(args, out var _); - var current = psi.Output(10_000); - Assert.Equal(current, VelopackRuntimeInfo.VelopackNugetVersion.ToString()); - } +namespace Velopack.Tests; - [Fact] - public void PlatformIsCorrect() - { +public class RuntimeInfoTests +{ + [Fact] + public void NugetVersionAgreesWithNbgv() + { + var args = new List { "get-version", "-v", "NuGetPackageVersion" }; + var psi = new ProcessStartInfo("nbgv"); + psi.AppendArgumentListSafe(args, out var _); + var current = psi.Output(10_000); + Assert.Equal(current, VelopackRuntimeInfo.VelopackNugetVersion.ToString()); + } + + [Fact] + public void PlatformIsCorrect() + { #if NETFRAMEWORK + Assert.True(VelopackRuntimeInfo.IsWindows); + Assert.Equal(RuntimeOs.Windows, VelopackRuntimeInfo.SystemOs); +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Assert.True(VelopackRuntimeInfo.IsWindows); Assert.Equal(RuntimeOs.Windows, VelopackRuntimeInfo.SystemOs); -#else - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - Assert.True(VelopackRuntimeInfo.IsWindows); - Assert.Equal(RuntimeOs.Windows, VelopackRuntimeInfo.SystemOs); - } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - Assert.True(VelopackRuntimeInfo.IsLinux); - Assert.Equal(RuntimeOs.Linux, VelopackRuntimeInfo.SystemOs); - } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - Assert.True(VelopackRuntimeInfo.IsOSX); - Assert.Equal(RuntimeOs.OSX, VelopackRuntimeInfo.SystemOs); - } else { - throw new PlatformNotSupportedException(); - } -#endif + } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { + Assert.True(VelopackRuntimeInfo.IsLinux); + Assert.Equal(RuntimeOs.Linux, VelopackRuntimeInfo.SystemOs); + } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + Assert.True(VelopackRuntimeInfo.IsOSX); + Assert.Equal(RuntimeOs.OSX, VelopackRuntimeInfo.SystemOs); + } else { + throw new PlatformNotSupportedException(); } +#endif } } diff --git a/test/Velopack.Tests/RuntimeTests.cs b/test/Velopack.Tests/RuntimeTests.cs index 2ade0689..be701596 100644 --- a/test/Velopack.Tests/RuntimeTests.cs +++ b/test/Velopack.Tests/RuntimeTests.cs @@ -2,107 +2,106 @@ using System.Net.Http; using Velopack.Windows; -namespace Velopack.Tests +namespace Velopack.Tests; + +public class RuntimeTests { - public class RuntimeTests + [Theory] + [InlineData("net6", "net6-x64-desktop")] + [InlineData("net6.0", "net6-x64-desktop")] + [InlineData("net6-x64", "net6-x64-desktop")] + [InlineData("net6-x86", "net6-x86-desktop")] + [InlineData("net3.1", "netcoreapp3.1-x64-desktop")] + [InlineData("netcoreapp3.1", "netcoreapp3.1-x64-desktop")] + [InlineData("net3.1-x86", "netcoreapp3.1-x86-desktop")] + [InlineData("net6.0.2", "net6.0.2-x64-desktop")] + [InlineData("net6.0.2-x86", "net6.0.2-x86-desktop")] + [InlineData("net6.0.1-x86", "net6.0.1-x86-desktop")] + [InlineData("net6.0.0", "net6-x64-desktop")] + [InlineData("net6.0-x64-desktop", "net6-x64-desktop")] + [InlineData("net7.0-x64-runtime", "net7-x64-runtime")] + [InlineData("net7.0-x64-asp", "net7-x64-asp")] + [InlineData("net7.0-desktop", "net7-x64-desktop")] + [InlineData("net7.0-runtime", "net7-x64-runtime")] + public void DotnetParsesValidVersions(string input, string expected) { - [Theory] - [InlineData("net6", "net6-x64-desktop")] - [InlineData("net6.0", "net6-x64-desktop")] - [InlineData("net6-x64", "net6-x64-desktop")] - [InlineData("net6-x86", "net6-x86-desktop")] - [InlineData("net3.1", "netcoreapp3.1-x64-desktop")] - [InlineData("netcoreapp3.1", "netcoreapp3.1-x64-desktop")] - [InlineData("net3.1-x86", "netcoreapp3.1-x86-desktop")] - [InlineData("net6.0.2", "net6.0.2-x64-desktop")] - [InlineData("net6.0.2-x86", "net6.0.2-x86-desktop")] - [InlineData("net6.0.1-x86", "net6.0.1-x86-desktop")] - [InlineData("net6.0.0", "net6-x64-desktop")] - [InlineData("net6.0-x64-desktop", "net6-x64-desktop")] - [InlineData("net7.0-x64-runtime", "net7-x64-runtime")] - [InlineData("net7.0-x64-asp", "net7-x64-asp")] - [InlineData("net7.0-desktop", "net7-x64-desktop")] - [InlineData("net7.0-runtime", "net7-x64-runtime")] - public void DotnetParsesValidVersions(string input, string expected) - { - var p = Runtimes.DotnetInfo.Parse(input); - Assert.Equal(expected, p.Id); - } + var p = Runtimes.DotnetInfo.Parse(input); + Assert.Equal(expected, p.Id); + } - [Theory] - [InlineData("net3.2")] - [InlineData("net4.9")] - [InlineData("net6.0.0.4")] - [InlineData("net7.0-x64-base")] - [InlineData("net6-basd")] - [InlineData("net6-x64-aakaka")] - public void DotnetParseThrowsInvalidVersion(string input) - { - Assert.ThrowsAny(() => Runtimes.DotnetInfo.Parse(input)); - } + [Theory] + [InlineData("net3.2")] + [InlineData("net4.9")] + [InlineData("net6.0.0.4")] + [InlineData("net7.0-x64-base")] + [InlineData("net6-basd")] + [InlineData("net6-x64-aakaka")] + public void DotnetParseThrowsInvalidVersion(string input) + { + Assert.ThrowsAny(() => Runtimes.DotnetInfo.Parse(input)); + } - [Theory] - [InlineData("net6", true)] - [InlineData("net20.0", true)] - [InlineData("net5.0.14-x86", true)] - [InlineData("netcoreapp3.1-x86", true)] - [InlineData("net48", true)] - [InlineData("netcoreapp4.8", false)] - [InlineData("net4.8", false)] - [InlineData("net2.5", false)] - [InlineData("vcredist110-x64", true)] - [InlineData("vcredist110-x86", true)] - [InlineData("vcredist110", true)] - [InlineData("vcredist143", true)] - [InlineData("asd", false)] - [InlineData("", false)] - [InlineData(null, false)] - [InlineData("net6-x64", true)] - [InlineData("net6-x64-runtime", true)] - [InlineData("net6-x64-desktop", true)] - public void GetRuntimeTests(string input, bool expected) - { - var dn = Runtimes.GetRuntimeByName(input); - Assert.Equal(expected, dn != null); - } + [Theory] + [InlineData("net6", true)] + [InlineData("net20.0", true)] + [InlineData("net5.0.14-x86", true)] + [InlineData("netcoreapp3.1-x86", true)] + [InlineData("net48", true)] + [InlineData("netcoreapp4.8", false)] + [InlineData("net4.8", false)] + [InlineData("net2.5", false)] + [InlineData("vcredist110-x64", true)] + [InlineData("vcredist110-x86", true)] + [InlineData("vcredist110", true)] + [InlineData("vcredist143", true)] + [InlineData("asd", false)] + [InlineData("", false)] + [InlineData(null, false)] + [InlineData("net6-x64", true)] + [InlineData("net6-x64-runtime", true)] + [InlineData("net6-x64-desktop", true)] + public void GetRuntimeTests(string input, bool expected) + { + var dn = Runtimes.GetRuntimeByName(input); + Assert.Equal(expected, dn != null); + } - [Theory(Skip = "Only run when needed")] - [InlineData("3.1", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.WindowsDesktop)] - [InlineData("3.1", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.Runtime)] - [InlineData("3.1", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.AspNetCore)] - [InlineData("3.1", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.WindowsDesktop)] - [InlineData("3.1", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.Runtime)] - [InlineData("3.1", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.AspNetCore)] - [InlineData("5.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.WindowsDesktop)] - [InlineData("5.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.Runtime)] - [InlineData("5.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.AspNetCore)] - [InlineData("5.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.WindowsDesktop)] - [InlineData("5.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.Runtime)] - [InlineData("5.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.AspNetCore)] - [InlineData("7.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.WindowsDesktop)] - [InlineData("7.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.Runtime)] - [InlineData("7.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.AspNetCore)] - [InlineData("7.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.WindowsDesktop)] - [InlineData("7.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.Runtime)] - [InlineData("7.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.AspNetCore)] - public async Task MicrosoftReturnsValidDotnetDownload(string minversion, RuntimeCpu architecture, Runtimes.DotnetRuntimeType runtimeType) - { - var dni = new Runtimes.DotnetInfo(minversion, architecture, runtimeType); - var url = await dni.GetDownloadUrl(); + [Theory(Skip = "Only run when needed")] + [InlineData("3.1", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.WindowsDesktop)] + [InlineData("3.1", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.Runtime)] + [InlineData("3.1", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.AspNetCore)] + [InlineData("3.1", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.WindowsDesktop)] + [InlineData("3.1", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.Runtime)] + [InlineData("3.1", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.AspNetCore)] + [InlineData("5.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.WindowsDesktop)] + [InlineData("5.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.Runtime)] + [InlineData("5.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.AspNetCore)] + [InlineData("5.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.WindowsDesktop)] + [InlineData("5.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.Runtime)] + [InlineData("5.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.AspNetCore)] + [InlineData("7.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.WindowsDesktop)] + [InlineData("7.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.Runtime)] + [InlineData("7.0", RuntimeCpu.x86, Runtimes.DotnetRuntimeType.AspNetCore)] + [InlineData("7.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.WindowsDesktop)] + [InlineData("7.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.Runtime)] + [InlineData("7.0", RuntimeCpu.x64, Runtimes.DotnetRuntimeType.AspNetCore)] + public async Task MicrosoftReturnsValidDotnetDownload(string minversion, RuntimeCpu architecture, Runtimes.DotnetRuntimeType runtimeType) + { + var dni = new Runtimes.DotnetInfo(minversion, architecture, runtimeType); + var url = await dni.GetDownloadUrl(); - Assert.Contains(minversion, url, StringComparison.OrdinalIgnoreCase); - Assert.Contains(architecture.ToString(), url, StringComparison.OrdinalIgnoreCase); + Assert.Contains(minversion, url, StringComparison.OrdinalIgnoreCase); + Assert.Contains(architecture.ToString(), url, StringComparison.OrdinalIgnoreCase); - if (runtimeType == Runtimes.DotnetRuntimeType.Runtime) - Assert.Matches(@"/dotnet-runtime-\d", url); - else if (runtimeType == Runtimes.DotnetRuntimeType.AspNetCore) - Assert.Matches(@"/aspnetcore-runtime-\d", url); - else if (runtimeType == Runtimes.DotnetRuntimeType.WindowsDesktop) - Assert.Matches(@"/windowsdesktop-runtime-\d", url); + if (runtimeType == Runtimes.DotnetRuntimeType.Runtime) + Assert.Matches(@"/dotnet-runtime-\d", url); + else if (runtimeType == Runtimes.DotnetRuntimeType.AspNetCore) + Assert.Matches(@"/aspnetcore-runtime-\d", url); + else if (runtimeType == Runtimes.DotnetRuntimeType.WindowsDesktop) + Assert.Matches(@"/windowsdesktop-runtime-\d", url); - using var hc = new HttpClient(); - var result = await hc.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); - result.EnsureSuccessStatusCode(); - } + using var hc = new HttpClient(); + var result = await hc.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + result.EnsureSuccessStatusCode(); } } \ No newline at end of file diff --git a/test/Velopack.Tests/ShortcutTests.cs b/test/Velopack.Tests/ShortcutTests.cs index 2c34056a..bb6d6af2 100644 --- a/test/Velopack.Tests/ShortcutTests.cs +++ b/test/Velopack.Tests/ShortcutTests.cs @@ -8,56 +8,55 @@ using System.Threading.Tasks; using Velopack.Locators; using Velopack.Windows; -namespace Velopack.Tests +namespace Velopack.Tests; + +[SupportedOSPlatform("windows")] +public class ShortcutTests { - [SupportedOSPlatform("windows")] - public class ShortcutTests + private readonly ITestOutputHelper _output; + + public ShortcutTests(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; + _output = output; + } - public ShortcutTests(ITestOutputHelper output) - { - _output = output; - } + [SkippableFact] + public void CanCreateAndRemoveShortcuts() + { + Skip.IfNot(VelopackRuntimeInfo.IsWindows); + using var logger = _output.BuildLoggerFor(); + string exeName = "NotSquirrelAwareApp.exe"; - [SkippableFact] - public void CanCreateAndRemoveShortcuts() - { - Skip.IfNot(VelopackRuntimeInfo.IsWindows); - using var logger = _output.BuildLoggerFor(); - string exeName = "NotSquirrelAwareApp.exe"; + using var _1 = Utility.GetTempDirectory(out var rootDir); + var packages = Directory.CreateDirectory(Path.Combine(rootDir, "packages")); + var current = Directory.CreateDirectory(Path.Combine(rootDir, "current")); - using var _1 = Utility.GetTempDirectory(out var rootDir); - var packages = Directory.CreateDirectory(Path.Combine(rootDir, "packages")); - var current = Directory.CreateDirectory(Path.Combine(rootDir, "current")); + PathHelper.CopyFixtureTo("AvaloniaCrossPlat-1.0.15-win-full.nupkg", packages.FullName); + PathHelper.CopyFixtureTo(exeName, current.FullName); - PathHelper.CopyFixtureTo("AvaloniaCrossPlat-1.0.15-win-full.nupkg", packages.FullName); - PathHelper.CopyFixtureTo(exeName, current.FullName); + var locator = new TestVelopackLocator("AvaloniaCrossPlat", "1.0.0", packages.FullName, current.FullName, rootDir, null, null, logger); + var sh = new Shortcuts(logger, locator); + var flag = ShortcutLocation.StartMenuRoot | ShortcutLocation.Desktop; + sh.DeleteShortcuts(exeName, flag); + sh.CreateShortcut(exeName, flag, false, ""); + var shortcuts = sh.FindShortcuts(exeName, flag); + Assert.Equal(2, shortcuts.Keys.Count); - var locator = new TestVelopackLocator("AvaloniaCrossPlat", "1.0.0", packages.FullName, current.FullName, rootDir, null, null, logger); - var sh = new Shortcuts(logger, locator); - var flag = ShortcutLocation.StartMenuRoot | ShortcutLocation.Desktop; - sh.DeleteShortcuts(exeName, flag); - sh.CreateShortcut(exeName, flag, false, ""); - var shortcuts = sh.FindShortcuts(exeName, flag); - Assert.Equal(2, shortcuts.Keys.Count); + var startDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs"); + var desktopDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop)); + var lnkName = "SquirrelAwareApp.lnk"; - var startDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs"); - var desktopDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop)); - var lnkName = "SquirrelAwareApp.lnk"; + var start = shortcuts[ShortcutLocation.StartMenuRoot]; + var desktop = shortcuts[ShortcutLocation.Desktop]; - var start = shortcuts[ShortcutLocation.StartMenuRoot]; - var desktop = shortcuts[ShortcutLocation.Desktop]; + var target = Path.Combine(current.FullName, exeName); + Assert.Equal(Path.Combine(startDir, lnkName), start.ShortCutFile); + Assert.Equal(target, start.Target); + Assert.Equal(Path.Combine(desktopDir, lnkName), desktop.ShortCutFile); + Assert.Equal(target, desktop.Target); - var target = Path.Combine(current.FullName, exeName); - Assert.Equal(Path.Combine(startDir, lnkName), start.ShortCutFile); - Assert.Equal(target, start.Target); - Assert.Equal(Path.Combine(desktopDir, lnkName), desktop.ShortCutFile); - Assert.Equal(target, desktop.Target); - - sh.DeleteShortcuts(exeName, flag); - var after = sh.FindShortcuts(exeName, flag); - Assert.Equal(0, after.Keys.Count); - } + sh.DeleteShortcuts(exeName, flag); + var after = sh.FindShortcuts(exeName, flag); + Assert.Equal(0, after.Keys.Count); } } diff --git a/test/Velopack.Tests/SimpleJsonTests.cs b/test/Velopack.Tests/SimpleJsonTests.cs index d5ca8f31..1610b08a 100644 --- a/test/Velopack.Tests/SimpleJsonTests.cs +++ b/test/Velopack.Tests/SimpleJsonTests.cs @@ -16,136 +16,135 @@ using SimpleJsonNameAttribute = System.Text.Json.Serialization.JsonPropertyNameA using SimpleJsonNameAttribute = Velopack.Json.JsonPropertyNameAttribute; #endif -namespace Velopack.Tests +namespace Velopack.Tests; + +public class SimpleJsonTests { - public class SimpleJsonTests + public static readonly JsonSerializerOptions Options = new JsonSerializerOptions { + AllowTrailingCommas = true, + ReadCommentHandling = JsonCommentHandling.Skip, + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter(), new SemanticVersionConverter() }, + }; + + internal class SemanticVersionConverter : JsonConverter { - public static readonly JsonSerializerOptions Options = new JsonSerializerOptions { - AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip, - PropertyNameCaseInsensitive = true, - Converters = { new JsonStringEnumConverter(), new SemanticVersionConverter() }, - }; - - internal class SemanticVersionConverter : JsonConverter + public override SemanticVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override SemanticVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return SemanticVersion.Parse(reader.GetString()); - } - - public override void Write(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.ToFullString()); - } + return SemanticVersion.Parse(reader.GetString()); } - [Fact] - public void JsonPropertyNameAttribueWrks() + public override void Write(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options) { - var obj = new TestGithubReleaseAsset { - UrlSomething = "https://ho", - BrowserDownloadUrl = "https://browser", - ContentType = "via", - }; - var json = JsonSerializer.Serialize(obj, Options); - var dez = SimpleJson.DeserializeObject(json); - Assert.Equal(obj.UrlSomething, dez.Url); - Assert.Equal(obj.BrowserDownloadUrl, dez.BrowserDownloadUrl); - Assert.Equal(obj.ContentType, dez.ContentType); - } - - [Fact] - public void JsonCanRoundTripComplexTypes() - { - var obj = new TestClass1 { - NameAsd = "hello", - UpcomingRelease = true, - ReleasedAt = DateTime.UtcNow, - Version = SemanticVersion.Parse("1.2.3-hello.23+metadata"), - AssetType = VelopackAssetType.Delta, - Greetings = new List { "hi", "there" }, - }; - var json = JsonSerializer.Serialize(obj, Options); - - Assert.Contains("\"Delta\"", json); - - var dez = SimpleJson.DeserializeObject(json); - Assert.Equal(obj.NameAsd, dez.nameAsd); - Assert.Equal(obj.UpcomingRelease, dez.upcomingRelease); - Assert.Equal(obj.ReleasedAt, dez.releasedAt); - Assert.Equal(obj.Version, dez.version); - Assert.Equal(obj.AssetType, dez.assetType); - Assert.Equal(obj.Greetings, dez.greetings); - } - - [Fact] - public void JsonCanParseReleasesJson() - { - var json = File.ReadAllText(PathHelper.GetFixture("testfeed.json")); - var feed = SimpleJson.DeserializeObject(json); - Assert.Equal(21, feed.Assets.Length); - Assert.True(feed.Assets.First().Version == new SemanticVersion(1, 0, 11)); - } - - public class TestGithubReleaseAsset - { - /// - /// The asset URL for this release asset. Requests to this URL will use API - /// quota and return JSON unless the 'Accept' header is "application/octet-stream". - /// - [JsonPropertyName("url")] - public string UrlSomething { get; set; } - - /// - /// The browser URL for this release asset. This does not use API quota, - /// however this URL only works for public repositories. If downloading - /// assets from a private repository, the property must - /// be used with an appropriate access token. - /// - [JsonPropertyName("browser_download_url")] - public string BrowserDownloadUrl { get; set; } - - /// The mime type of this release asset (as detected by GitHub). - [JsonPropertyName("content_type")] - public string ContentType { get; set; } - } - - - internal class TestClass1 - { - public string NameAsd { get; set; } - - [JsonPropertyName("upcoming_release888")] - public bool UpcomingRelease { get; set; } - - [JsonPropertyName("released_at")] - public DateTime ReleasedAt { get; set; } - - public SemanticVersion Version { get; set; } - - [JsonPropertyName("t")] - public VelopackAssetType AssetType { get; set; } - - public List Greetings { get; set; } - } - - internal class TestClass2 - { - public string nameAsd { get; set; } - - [SimpleJsonName("upcoming_release888")] - public bool upcomingRelease { get; set; } - - [SimpleJsonName("released_at")] - public DateTime releasedAt { get; set; } - - public SemanticVersion version { get; set; } - - [SimpleJsonName("t")] - public VelopackAssetType assetType { get; set; } - - public List greetings { get; set; } + writer.WriteStringValue(value.ToFullString()); } } + + [Fact] + public void JsonPropertyNameAttribueWrks() + { + var obj = new TestGithubReleaseAsset { + UrlSomething = "https://ho", + BrowserDownloadUrl = "https://browser", + ContentType = "via", + }; + var json = JsonSerializer.Serialize(obj, Options); + var dez = SimpleJson.DeserializeObject(json); + Assert.Equal(obj.UrlSomething, dez.Url); + Assert.Equal(obj.BrowserDownloadUrl, dez.BrowserDownloadUrl); + Assert.Equal(obj.ContentType, dez.ContentType); + } + + [Fact] + public void JsonCanRoundTripComplexTypes() + { + var obj = new TestClass1 { + NameAsd = "hello", + UpcomingRelease = true, + ReleasedAt = DateTime.UtcNow, + Version = SemanticVersion.Parse("1.2.3-hello.23+metadata"), + AssetType = VelopackAssetType.Delta, + Greetings = new List { "hi", "there" }, + }; + var json = JsonSerializer.Serialize(obj, Options); + + Assert.Contains("\"Delta\"", json); + + var dez = SimpleJson.DeserializeObject(json); + Assert.Equal(obj.NameAsd, dez.nameAsd); + Assert.Equal(obj.UpcomingRelease, dez.upcomingRelease); + Assert.Equal(obj.ReleasedAt, dez.releasedAt); + Assert.Equal(obj.Version, dez.version); + Assert.Equal(obj.AssetType, dez.assetType); + Assert.Equal(obj.Greetings, dez.greetings); + } + + [Fact] + public void JsonCanParseReleasesJson() + { + var json = File.ReadAllText(PathHelper.GetFixture("testfeed.json")); + var feed = SimpleJson.DeserializeObject(json); + Assert.Equal(21, feed.Assets.Length); + Assert.True(feed.Assets.First().Version == new SemanticVersion(1, 0, 11)); + } + + public class TestGithubReleaseAsset + { + /// + /// The asset URL for this release asset. Requests to this URL will use API + /// quota and return JSON unless the 'Accept' header is "application/octet-stream". + /// + [JsonPropertyName("url")] + public string UrlSomething { get; set; } + + /// + /// The browser URL for this release asset. This does not use API quota, + /// however this URL only works for public repositories. If downloading + /// assets from a private repository, the property must + /// be used with an appropriate access token. + /// + [JsonPropertyName("browser_download_url")] + public string BrowserDownloadUrl { get; set; } + + /// The mime type of this release asset (as detected by GitHub). + [JsonPropertyName("content_type")] + public string ContentType { get; set; } + } + + + internal class TestClass1 + { + public string NameAsd { get; set; } + + [JsonPropertyName("upcoming_release888")] + public bool UpcomingRelease { get; set; } + + [JsonPropertyName("released_at")] + public DateTime ReleasedAt { get; set; } + + public SemanticVersion Version { get; set; } + + [JsonPropertyName("t")] + public VelopackAssetType AssetType { get; set; } + + public List Greetings { get; set; } + } + + internal class TestClass2 + { + public string nameAsd { get; set; } + + [SimpleJsonName("upcoming_release888")] + public bool upcomingRelease { get; set; } + + [SimpleJsonName("released_at")] + public DateTime releasedAt { get; set; } + + public SemanticVersion version { get; set; } + + [SimpleJsonName("t")] + public VelopackAssetType assetType { get; set; } + + public List greetings { get; set; } + } } diff --git a/test/Velopack.Tests/TestHelpers/AssertExtensions.cs b/test/Velopack.Tests/TestHelpers/AssertExtensions.cs index 6a2e57d6..d6637124 100644 --- a/test/Velopack.Tests/TestHelpers/AssertExtensions.cs +++ b/test/Velopack.Tests/TestHelpers/AssertExtensions.cs @@ -1,147 +1,146 @@ using System.Collections; using System.Globalization; -namespace Velopack.Tests.TestHelpers +namespace Velopack.Tests.TestHelpers; + +public static class AssertExtensions { - public static class AssertExtensions + public static void ShouldBeAboutEqualTo(this DateTimeOffset expected, DateTimeOffset current) { - public static void ShouldBeAboutEqualTo(this DateTimeOffset expected, DateTimeOffset current) - { - Assert.Equal(expected.Date, current.Date); - Assert.Equal(expected.Offset, current.Offset); - Assert.Equal(expected.Hour, current.Hour); - Assert.Equal(expected.Minute, current.Minute); - Assert.Equal(expected.Second, current.Second); - } - - public static void ShouldBeFalse(this bool currentObject) - { - Assert.False(currentObject); - } - - public static void ShouldBeNull(this object currentObject) - { - Assert.Null(currentObject); - } - - public static void ShouldBeEmpty(this IEnumerable items) - { - Assert.Empty(items); - } - - public static void ShouldNotBeEmpty(this IEnumerable items) - { - Assert.NotEmpty(items); - } - - public static void ShouldBeTrue(this bool currentObject) - { - Assert.True(currentObject); - } - - public static void ShouldEqual(this object compareFrom, object compareTo) - { - Assert.Equal(compareTo, compareFrom); - } - - public static void ShouldEqual(this T compareFrom, T compareTo) - { - Assert.Equal(compareTo, compareFrom); - } - - public static void ShouldBeSameAs(this T actual, T expected) - { - Assert.Same(expected, actual); - } - - public static void ShouldNotBeSameAs(this T actual, T expected) - { - Assert.NotSame(expected, actual); - } - - public static void ShouldBeAssignableFrom(this object instance) where T : class - { - Assert.IsAssignableFrom(instance); - } - - public static void ShouldBeType(this object instance, Type type) - { - Assert.IsType(type, instance); - } - - public static void ShouldBeType(this object instance) - { - Assert.IsType(instance); - } - - public static void ShouldNotBeType(this object instance) - { - Assert.IsNotType(instance); - } - - public static void ShouldContain(this string current, string expectedSubstring, StringComparison comparison) - { - Assert.Contains(expectedSubstring, current, comparison); - } - - public static void ShouldStartWith(this string current, string expectedSubstring, StringComparison comparison) - { - Assert.True(current.StartsWith(expectedSubstring, comparison)); - } - - public static void ShouldNotBeNull(this object currentObject) - { - Assert.NotNull(currentObject); - } - - public static void ShouldNotBeNullNorEmpty(this string value) - { - Assert.NotNull(value); - Assert.NotEmpty(value); - } - - public static void ShouldNotEqual(this object compareFrom, object compareTo) - { - Assert.NotEqual(compareTo, compareFrom); - } - - public static void ShouldBeGreaterThan(this T current, T other) where T : IComparable - { - Assert.True(current.CompareTo(other) > 0, current + " is not greater than " + other); - } - - public static void ShouldBeLessThan(this T current, T other) where T : IComparable - { - Assert.True(current.CompareTo(other) < 0, current + " is not less than " + other); - } - - static string ToSafeString(this char c) - { - if (Char.IsControl(c) || Char.IsWhiteSpace(c)) { - switch (c) { - case '\r': - return @"\r"; - case '\n': - return @"\n"; - case '\t': - return @"\t"; - case '\a': - return @"\a"; - case '\v': - return @"\v"; - case '\f': - return @"\f"; - default: - return String.Format("\\u{0:X};", (int) c); - } - } - return c.ToString(CultureInfo.InvariantCulture); - } + Assert.Equal(expected.Date, current.Date); + Assert.Equal(expected.Offset, current.Offset); + Assert.Equal(expected.Hour, current.Hour); + Assert.Equal(expected.Minute, current.Minute); + Assert.Equal(expected.Second, current.Second); } - public enum DiffStyle + public static void ShouldBeFalse(this bool currentObject) { - Full, - Minimal + Assert.False(currentObject); + } + + public static void ShouldBeNull(this object currentObject) + { + Assert.Null(currentObject); + } + + public static void ShouldBeEmpty(this IEnumerable items) + { + Assert.Empty(items); + } + + public static void ShouldNotBeEmpty(this IEnumerable items) + { + Assert.NotEmpty(items); + } + + public static void ShouldBeTrue(this bool currentObject) + { + Assert.True(currentObject); + } + + public static void ShouldEqual(this object compareFrom, object compareTo) + { + Assert.Equal(compareTo, compareFrom); + } + + public static void ShouldEqual(this T compareFrom, T compareTo) + { + Assert.Equal(compareTo, compareFrom); + } + + public static void ShouldBeSameAs(this T actual, T expected) + { + Assert.Same(expected, actual); + } + + public static void ShouldNotBeSameAs(this T actual, T expected) + { + Assert.NotSame(expected, actual); + } + + public static void ShouldBeAssignableFrom(this object instance) where T : class + { + Assert.IsAssignableFrom(instance); + } + + public static void ShouldBeType(this object instance, Type type) + { + Assert.IsType(type, instance); + } + + public static void ShouldBeType(this object instance) + { + Assert.IsType(instance); + } + + public static void ShouldNotBeType(this object instance) + { + Assert.IsNotType(instance); + } + + public static void ShouldContain(this string current, string expectedSubstring, StringComparison comparison) + { + Assert.Contains(expectedSubstring, current, comparison); + } + + public static void ShouldStartWith(this string current, string expectedSubstring, StringComparison comparison) + { + Assert.True(current.StartsWith(expectedSubstring, comparison)); + } + + public static void ShouldNotBeNull(this object currentObject) + { + Assert.NotNull(currentObject); + } + + public static void ShouldNotBeNullNorEmpty(this string value) + { + Assert.NotNull(value); + Assert.NotEmpty(value); + } + + public static void ShouldNotEqual(this object compareFrom, object compareTo) + { + Assert.NotEqual(compareTo, compareFrom); + } + + public static void ShouldBeGreaterThan(this T current, T other) where T : IComparable + { + Assert.True(current.CompareTo(other) > 0, current + " is not greater than " + other); + } + + public static void ShouldBeLessThan(this T current, T other) where T : IComparable + { + Assert.True(current.CompareTo(other) < 0, current + " is not less than " + other); + } + + static string ToSafeString(this char c) + { + if (Char.IsControl(c) || Char.IsWhiteSpace(c)) { + switch (c) { + case '\r': + return @"\r"; + case '\n': + return @"\n"; + case '\t': + return @"\t"; + case '\a': + return @"\a"; + case '\v': + return @"\v"; + case '\f': + return @"\f"; + default: + return String.Format("\\u{0:X};", (int) c); + } + } + return c.ToString(CultureInfo.InvariantCulture); } } + +public enum DiffStyle +{ + Full, + Minimal +} diff --git a/test/Velopack.Tests/TestHelpers/ExposedClass.cs b/test/Velopack.Tests/TestHelpers/ExposedClass.cs index 2e02a6d1..4de100c6 100644 --- a/test/Velopack.Tests/TestHelpers/ExposedClass.cs +++ b/test/Velopack.Tests/TestHelpers/ExposedClass.cs @@ -6,133 +6,132 @@ using System.Reflection; // Lovingly stolen from http://exposedobject.codeplex.com/ -namespace Velopack.Tests.TestHelpers +namespace Velopack.Tests.TestHelpers; + +public class ExposedClass : DynamicObject { - public class ExposedClass : DynamicObject + private Type m_type; + private Dictionary>> m_staticMethods; + private Dictionary>> m_genStaticMethods; + + private ExposedClass(Type type) { - private Type m_type; - private Dictionary>> m_staticMethods; - private Dictionary>> m_genStaticMethods; + m_type = type; - private ExposedClass(Type type) + m_staticMethods = + m_type + .GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static) + .Where(m => !m.IsGenericMethod) + .GroupBy(m => m.Name) + .ToDictionary( + p => p.Key, + p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList())); + + m_genStaticMethods = + m_type + .GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static) + .Where(m => m.IsGenericMethod) + .GroupBy(m => m.Name) + .ToDictionary( + p => p.Key, + p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList())); + } + + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + // Get type args of the call + Type[] typeArgs = ExposedObjectHelper.GetTypeArgs(binder); + if (typeArgs != null && typeArgs.Length == 0) typeArgs = null; + + // + // Try to call a non-generic instance method + // + if (typeArgs == null + && m_staticMethods.ContainsKey(binder.Name) + && m_staticMethods[binder.Name].ContainsKey(args.Length) + && ExposedObjectHelper.InvokeBestMethod(args, null, m_staticMethods[binder.Name][args.Length], out result)) { - m_type = type; - - m_staticMethods = - m_type - .GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static) - .Where(m => !m.IsGenericMethod) - .GroupBy(m => m.Name) - .ToDictionary( - p => p.Key, - p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList())); - - m_genStaticMethods = - m_type - .GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static) - .Where(m => m.IsGenericMethod) - .GroupBy(m => m.Name) - .ToDictionary( - p => p.Key, - p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList())); + return true; } - public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + // + // Try to call a generic instance method + // + if (m_staticMethods.ContainsKey(binder.Name) + && m_staticMethods[binder.Name].ContainsKey(args.Length)) { - // Get type args of the call - Type[] typeArgs = ExposedObjectHelper.GetTypeArgs(binder); - if (typeArgs != null && typeArgs.Length == 0) typeArgs = null; + List methods = new List(); - // - // Try to call a non-generic instance method - // - if (typeArgs == null - && m_staticMethods.ContainsKey(binder.Name) - && m_staticMethods[binder.Name].ContainsKey(args.Length) - && ExposedObjectHelper.InvokeBestMethod(args, null, m_staticMethods[binder.Name][args.Length], out result)) + foreach (var method in m_genStaticMethods[binder.Name][args.Length]) { - return true; - } - - // - // Try to call a generic instance method - // - if (m_staticMethods.ContainsKey(binder.Name) - && m_staticMethods[binder.Name].ContainsKey(args.Length)) - { - List methods = new List(); - - foreach (var method in m_genStaticMethods[binder.Name][args.Length]) + if (method.GetGenericArguments().Length == typeArgs.Length) { - if (method.GetGenericArguments().Length == typeArgs.Length) - { - methods.Add(method.MakeGenericMethod(typeArgs)); - } - } - - if (ExposedObjectHelper.InvokeBestMethod(args, null, methods, out result)) - { - return true; + methods.Add(method.MakeGenericMethod(typeArgs)); } } - result = null; - return false; + if (ExposedObjectHelper.InvokeBestMethod(args, null, methods, out result)) + { + return true; + } } - public override bool TrySetMember(SetMemberBinder binder, object value) + + result = null; + return false; + } + public override bool TrySetMember(SetMemberBinder binder, object value) + { + var propertyInfo = m_type.GetProperty( + binder.Name, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + + if (propertyInfo != null) { - var propertyInfo = m_type.GetProperty( - binder.Name, - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); - - if (propertyInfo != null) - { - propertyInfo.SetValue(null, value, null); - return true; - } - - var fieldInfo = m_type.GetField( - binder.Name, - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); - - if (fieldInfo != null) - { - fieldInfo.SetValue(null, value); - return true; - } - - return false; + propertyInfo.SetValue(null, value, null); + return true; } - public override bool TryGetMember(GetMemberBinder binder, out object result) + var fieldInfo = m_type.GetField( + binder.Name, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + + if (fieldInfo != null) { - var propertyInfo = m_type.GetProperty( - binder.Name, - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); - - if (propertyInfo != null) - { - result = propertyInfo.GetValue(null, null); - return true; - } - - var fieldInfo = m_type.GetField( - binder.Name, - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); - - if (fieldInfo != null) - { - result = fieldInfo.GetValue(null); - return true; - } - - result = null; - return false; + fieldInfo.SetValue(null, value); + return true; } - public static dynamic From(Type type) + return false; + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + var propertyInfo = m_type.GetProperty( + binder.Name, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + + if (propertyInfo != null) { - return new ExposedClass(type); + result = propertyInfo.GetValue(null, null); + return true; } + + var fieldInfo = m_type.GetField( + binder.Name, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); + + if (fieldInfo != null) + { + result = fieldInfo.GetValue(null); + return true; + } + + result = null; + return false; + } + + public static dynamic From(Type type) + { + return new ExposedClass(type); } } diff --git a/test/Velopack.Tests/TestHelpers/ExposedObject.cs b/test/Velopack.Tests/TestHelpers/ExposedObject.cs index 49420527..5e703208 100644 --- a/test/Velopack.Tests/TestHelpers/ExposedObject.cs +++ b/test/Velopack.Tests/TestHelpers/ExposedObject.cs @@ -7,164 +7,162 @@ using System.Reflection; // Lovingly stolen from http://exposedobject.codeplex.com/ -namespace Velopack.Tests.TestHelpers +namespace Velopack.Tests.TestHelpers; + +public class ExposedObject : DynamicObject { - public class ExposedObject : DynamicObject + private object m_object; + private Type m_type; + private Dictionary>> m_instanceMethods; + private Dictionary>> m_genInstanceMethods; + + private ExposedObject(object obj) { - private object m_object; - private Type m_type; - private Dictionary>> m_instanceMethods; - private Dictionary>> m_genInstanceMethods; + m_object = obj; + m_type = obj.GetType(); - private ExposedObject(object obj) - { - m_object = obj; - m_type = obj.GetType(); + m_instanceMethods = + m_type + .GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) + .Where(m => !m.IsGenericMethod) + .GroupBy(m => m.Name) + .ToDictionary( + p => p.Key, + p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList())); - m_instanceMethods = - m_type - .GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) - .Where(m => !m.IsGenericMethod) - .GroupBy(m => m.Name) - .ToDictionary( - p => p.Key, - p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList())); - - m_genInstanceMethods = - m_type - .GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) - .Where(m => m.IsGenericMethod) - .GroupBy(m => m.Name) - .ToDictionary( - p => p.Key, - p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList())); - } - - public object Object { get { return m_object; } } - - public static dynamic New() - { - return New(typeof(T)); - } - - public static dynamic New(Type type) - { - return new ExposedObject(Activator.CreateInstance(type)); - } - - public static dynamic From(object obj) - { - return new ExposedObject(obj); - } - - public static T Cast(ExposedObject t) - { - return (T)t.m_object; - } - - public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) - { - // Get type args of the call - Type[] typeArgs = ExposedObjectHelper.GetTypeArgs(binder); - if (typeArgs != null && typeArgs.Length == 0) typeArgs = null; - - // - // Try to call a non-generic instance method - // - if (typeArgs == null - && m_instanceMethods.ContainsKey(binder.Name) - && m_instanceMethods[binder.Name].ContainsKey(args.Length) - && ExposedObjectHelper.InvokeBestMethod(args, m_object, m_instanceMethods[binder.Name][args.Length], out result)) - { - return true; - } - - // - // Try to call a generic instance method - // - if (m_instanceMethods.ContainsKey(binder.Name) - && m_instanceMethods[binder.Name].ContainsKey(args.Length)) - { - List methods = new List(); - - if (m_genInstanceMethods.ContainsKey(binder.Name) && - m_genInstanceMethods[binder.Name].ContainsKey(args.Length)) - { - foreach (var method in m_genInstanceMethods[binder.Name][args.Length]) - { - if (method.GetGenericArguments().Length == typeArgs.Length) - { - methods.Add(method.MakeGenericMethod(typeArgs)); - } - } - } - - if (ExposedObjectHelper.InvokeBestMethod(args, m_object, methods, out result)) - { - return true; - } - } - - result = null; - return false; - } - - public override bool TrySetMember(SetMemberBinder binder, object value) - { - var propertyInfo = m_type.GetProperty( - binder.Name, - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); - - if (propertyInfo != null) - { - propertyInfo.SetValue(m_object, value, null); - return true; - } - - var fieldInfo = m_type.GetField( - binder.Name, - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); - - if (fieldInfo != null) - { - fieldInfo.SetValue(m_object, value); - return true; - } - - return false; - } - - public override bool TryGetMember(GetMemberBinder binder, out object result) - { - var propertyInfo = m_object.GetType().GetProperty( - binder.Name, - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); - - if (propertyInfo != null) - { - result = propertyInfo.GetValue(m_object, null); - return true; - } - - var fieldInfo = m_object.GetType().GetField( - binder.Name, - BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); - - if (fieldInfo != null) - { - result = fieldInfo.GetValue(m_object); - return true; - } - - result = null; - return false; - } - - public override bool TryConvert(ConvertBinder binder, out object result) - { - result = m_object; - return true; - } + m_genInstanceMethods = + m_type + .GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) + .Where(m => m.IsGenericMethod) + .GroupBy(m => m.Name) + .ToDictionary( + p => p.Key, + p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList())); } + public object Object { get { return m_object; } } + + public static dynamic New() + { + return New(typeof(T)); + } + + public static dynamic New(Type type) + { + return new ExposedObject(Activator.CreateInstance(type)); + } + + public static dynamic From(object obj) + { + return new ExposedObject(obj); + } + + public static T Cast(ExposedObject t) + { + return (T)t.m_object; + } + + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + // Get type args of the call + Type[] typeArgs = ExposedObjectHelper.GetTypeArgs(binder); + if (typeArgs != null && typeArgs.Length == 0) typeArgs = null; + + // + // Try to call a non-generic instance method + // + if (typeArgs == null + && m_instanceMethods.ContainsKey(binder.Name) + && m_instanceMethods[binder.Name].ContainsKey(args.Length) + && ExposedObjectHelper.InvokeBestMethod(args, m_object, m_instanceMethods[binder.Name][args.Length], out result)) + { + return true; + } + + // + // Try to call a generic instance method + // + if (m_instanceMethods.ContainsKey(binder.Name) + && m_instanceMethods[binder.Name].ContainsKey(args.Length)) + { + List methods = new List(); + + if (m_genInstanceMethods.ContainsKey(binder.Name) && + m_genInstanceMethods[binder.Name].ContainsKey(args.Length)) + { + foreach (var method in m_genInstanceMethods[binder.Name][args.Length]) + { + if (method.GetGenericArguments().Length == typeArgs.Length) + { + methods.Add(method.MakeGenericMethod(typeArgs)); + } + } + } + + if (ExposedObjectHelper.InvokeBestMethod(args, m_object, methods, out result)) + { + return true; + } + } + + result = null; + return false; + } + + public override bool TrySetMember(SetMemberBinder binder, object value) + { + var propertyInfo = m_type.GetProperty( + binder.Name, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); + + if (propertyInfo != null) + { + propertyInfo.SetValue(m_object, value, null); + return true; + } + + var fieldInfo = m_type.GetField( + binder.Name, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); + + if (fieldInfo != null) + { + fieldInfo.SetValue(m_object, value); + return true; + } + + return false; + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + var propertyInfo = m_object.GetType().GetProperty( + binder.Name, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); + + if (propertyInfo != null) + { + result = propertyInfo.GetValue(m_object, null); + return true; + } + + var fieldInfo = m_object.GetType().GetField( + binder.Name, + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); + + if (fieldInfo != null) + { + result = fieldInfo.GetValue(m_object); + return true; + } + + result = null; + return false; + } + + public override bool TryConvert(ConvertBinder binder, out object result) + { + result = m_object; + return true; + } } diff --git a/test/Velopack.Tests/TestHelpers/ExposedObjectHelper.cs b/test/Velopack.Tests/TestHelpers/ExposedObjectHelper.cs index 8ad02824..a7209eae 100644 --- a/test/Velopack.Tests/TestHelpers/ExposedObjectHelper.cs +++ b/test/Velopack.Tests/TestHelpers/ExposedObjectHelper.cs @@ -6,89 +6,88 @@ using System.Dynamic; // Lovingly stolen from http://exposedobject.codeplex.com/ -namespace Velopack.Tests.TestHelpers +namespace Velopack.Tests.TestHelpers; + +internal class ExposedObjectHelper { - internal class ExposedObjectHelper + private static Type s_csharpInvokePropertyType = + typeof(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException) + .Assembly + .GetType("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder"); + + internal static bool InvokeBestMethod(object[] args, object target, List instanceMethods, out object result) { - private static Type s_csharpInvokePropertyType = - typeof(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException) - .Assembly - .GetType("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder"); - - internal static bool InvokeBestMethod(object[] args, object target, List instanceMethods, out object result) + if (instanceMethods.Count == 1) { - if (instanceMethods.Count == 1) + // Just one matching instance method - call it + if (TryInvoke(instanceMethods[0], target, args, out result)) { - // Just one matching instance method - call it - if (TryInvoke(instanceMethods[0], target, args, out result)) - { - return true; - } - } - else if (instanceMethods.Count > 1) - { - // Find a method with best matching parameters - MethodInfo best = null; - Type[] bestParams = null; - Type[] actualParams = args.Select(p => p == null ? typeof(object) : p.GetType()).ToArray(); - - Func isAssignableFrom = (a, b) => - { - for (int i = 0; i < a.Length; i++) - { - if (!a[i].IsAssignableFrom(b[i])) return false; - } - return true; - }; - - - foreach (var method in instanceMethods.Where(m => m.GetParameters().Length == args.Length)) - { - Type[] mParams = method.GetParameters().Select(x => x.ParameterType).ToArray(); - if (isAssignableFrom(mParams, actualParams)) - { - if (best == null || isAssignableFrom(bestParams, mParams)) - { - best = method; - bestParams = mParams; - } - } - } - - if (best != null && TryInvoke(best, target, args, out result)) - { - return true; - } - } - - result = null; - return false; - } - - internal static bool TryInvoke(MethodInfo methodInfo, object target, object[] args, out object result) - { - try - { - result = methodInfo.Invoke(target, args); return true; } - catch (TargetInvocationException) { } - catch (TargetParameterCountException) { } - - result = null; - return false; - } - - internal static Type[] GetTypeArgs(InvokeMemberBinder binder) + else if (instanceMethods.Count > 1) { - if (s_csharpInvokePropertyType.IsInstanceOfType(binder)) + // Find a method with best matching parameters + MethodInfo best = null; + Type[] bestParams = null; + Type[] actualParams = args.Select(p => p == null ? typeof(object) : p.GetType()).ToArray(); + + Func isAssignableFrom = (a, b) => { - PropertyInfo typeArgsProperty = s_csharpInvokePropertyType.GetProperty("TypeArguments"); - return ((IEnumerable)typeArgsProperty.GetValue(binder, null)).ToArray(); + for (int i = 0; i < a.Length; i++) + { + if (!a[i].IsAssignableFrom(b[i])) return false; + } + return true; + }; + + + foreach (var method in instanceMethods.Where(m => m.GetParameters().Length == args.Length)) + { + Type[] mParams = method.GetParameters().Select(x => x.ParameterType).ToArray(); + if (isAssignableFrom(mParams, actualParams)) + { + if (best == null || isAssignableFrom(bestParams, mParams)) + { + best = method; + bestParams = mParams; + } + } + } + + if (best != null && TryInvoke(best, target, args, out result)) + { + return true; } - return null; } + result = null; + return false; + } + + internal static bool TryInvoke(MethodInfo methodInfo, object target, object[] args, out object result) + { + try + { + result = methodInfo.Invoke(target, args); + return true; + } + catch (TargetInvocationException) { } + catch (TargetParameterCountException) { } + + result = null; + return false; + } + + internal static Type[] GetTypeArgs(InvokeMemberBinder binder) + { + if (s_csharpInvokePropertyType.IsInstanceOfType(binder)) + { + PropertyInfo typeArgsProperty = s_csharpInvokePropertyType.GetProperty("TypeArguments"); + return ((IEnumerable)typeArgsProperty.GetValue(binder, null)).ToArray(); + } + return null; + } + } diff --git a/test/Velopack.Tests/TestHelpers/FakeDownloader.cs b/test/Velopack.Tests/TestHelpers/FakeDownloader.cs index 8ee7fa63..094ba920 100644 --- a/test/Velopack.Tests/TestHelpers/FakeDownloader.cs +++ b/test/Velopack.Tests/TestHelpers/FakeDownloader.cs @@ -1,39 +1,38 @@ using System.Text; -namespace Velopack.Tests +namespace Velopack.Tests; + +public class FakeDownloader : Sources.IFileDownloader { - public class FakeDownloader : Sources.IFileDownloader + public string LastUrl { get; private set; } + public string LastLocalFile { get; private set; } + public string LastAuthHeader { get; private set; } + public string LastAcceptHeader { get; private set; } + public byte[] MockedResponseBytes { get; set; } = new byte[0]; + public bool WriteMockLocalFile { get; set; } = false; + + public Task DownloadBytes(string url, string auth, string acc) { - public string LastUrl { get; private set; } - public string LastLocalFile { get; private set; } - public string LastAuthHeader { get; private set; } - public string LastAcceptHeader { get; private set; } - public byte[] MockedResponseBytes { get; set; } = new byte[0]; - public bool WriteMockLocalFile { get; set; } = false; + LastUrl = url; + LastAuthHeader = auth; + LastAcceptHeader = acc; + return Task.FromResult(MockedResponseBytes); + } - public Task DownloadBytes(string url, string auth, string acc) - { - LastUrl = url; - LastAuthHeader = auth; - LastAcceptHeader = acc; - return Task.FromResult(MockedResponseBytes); - } + public async Task DownloadFile(string url, string targetFile, Action progress, string auth, string acc, CancellationToken token) + { + LastLocalFile = targetFile; + var resp = await DownloadBytes(url, auth, acc); + progress?.Invoke(25); + progress?.Invoke(50); + progress?.Invoke(75); + progress?.Invoke(100); + if (WriteMockLocalFile) + File.WriteAllBytes(targetFile, resp); + } - public async Task DownloadFile(string url, string targetFile, Action progress, string auth, string acc, CancellationToken token) - { - LastLocalFile = targetFile; - var resp = await DownloadBytes(url, auth, acc); - progress?.Invoke(25); - progress?.Invoke(50); - progress?.Invoke(75); - progress?.Invoke(100); - if (WriteMockLocalFile) - File.WriteAllBytes(targetFile, resp); - } - - public async Task DownloadString(string url, string auth, string acc) - { - return Encoding.UTF8.GetString(await DownloadBytes(url, auth, acc)); - } + public async Task DownloadString(string url, string auth, string acc) + { + return Encoding.UTF8.GetString(await DownloadBytes(url, auth, acc)); } } diff --git a/test/Velopack.Tests/TestHelpers/FakeFixtureRepository.cs b/test/Velopack.Tests/TestHelpers/FakeFixtureRepository.cs index 70176283..bea8c231 100644 --- a/test/Velopack.Tests/TestHelpers/FakeFixtureRepository.cs +++ b/test/Velopack.Tests/TestHelpers/FakeFixtureRepository.cs @@ -4,111 +4,110 @@ using System.Text; using System.Text.Json; using Velopack.Sources; -namespace Velopack.Tests.TestHelpers +namespace Velopack.Tests.TestHelpers; + +internal class FakeFixtureRepository : Sources.IFileDownloader { - internal class FakeFixtureRepository : Sources.IFileDownloader + private readonly string _pkgId; + private readonly IEnumerable _releases; + private readonly VelopackAssetFeed _releasesNew; + private readonly string _releasesName; + private readonly string _releasesNameNew; + + public FakeFixtureRepository(string pkgId, bool mockLatestFullVer, string channel = null) { - private readonly string _pkgId; - private readonly IEnumerable _releases; - private readonly VelopackAssetFeed _releasesNew; - private readonly string _releasesName; - private readonly string _releasesNameNew; + _releasesName = Utility.GetReleasesFileName(channel); + _releasesNameNew = Utility.GetVeloReleaseIndexName(channel); + _pkgId = pkgId; + var releases = ReleaseEntry.BuildReleasesFile(PathHelper.GetFixturesDir(), false) + .Where(r => r.OriginalFilename.StartsWith(_pkgId)) + .ToList(); - public FakeFixtureRepository(string pkgId, bool mockLatestFullVer, string channel = null) - { - _releasesName = Utility.GetReleasesFileName(channel); - _releasesNameNew = Utility.GetVeloReleaseIndexName(channel); - _pkgId = pkgId; - var releases = ReleaseEntry.BuildReleasesFile(PathHelper.GetFixturesDir(), false) - .Where(r => r.OriginalFilename.StartsWith(_pkgId)) - .ToList(); + var releasesNew = new SimpleFileSource(new DirectoryInfo(PathHelper.GetFixturesDir())) + .GetReleaseFeed(NullLogger.Instance, null).GetAwaiterResult().Assets + .Where(r => r.FileName.StartsWith(_pkgId)) + .ToList(); - var releasesNew = new SimpleFileSource(new DirectoryInfo(PathHelper.GetFixturesDir())) - .GetReleaseFeed(NullLogger.Instance, null).GetAwaiterResult().Assets - .Where(r => r.FileName.StartsWith(_pkgId)) - .ToList(); + if (mockLatestFullVer) { + var minFullVer = releases.Where(r => !r.IsDelta).OrderBy(r => r.Version).First(); + var maxfullVer = releases.Where(r => !r.IsDelta).OrderByDescending(r => r.Version).First(); + var maxDeltaVer = releases.Where(r => r.IsDelta).OrderByDescending(r => r.Version).First(); - if (mockLatestFullVer) { - var minFullVer = releases.Where(r => !r.IsDelta).OrderBy(r => r.Version).First(); - var maxfullVer = releases.Where(r => !r.IsDelta).OrderByDescending(r => r.Version).First(); - var maxDeltaVer = releases.Where(r => r.IsDelta).OrderByDescending(r => r.Version).First(); + // our fixtures don't have a full package for the latest version, we expect the tests to generate this file + if (maxfullVer.Version < maxDeltaVer.Version) { + var name = new ReleaseEntryName(maxfullVer.PackageId, maxDeltaVer.Version, false); + releases.Add(new ReleaseEntry("0000000000000000000000000000000000000000", name.ToFileName(), maxfullVer.Filesize)); - // our fixtures don't have a full package for the latest version, we expect the tests to generate this file - if (maxfullVer.Version < maxDeltaVer.Version) { - var name = new ReleaseEntryName(maxfullVer.PackageId, maxDeltaVer.Version, false); - releases.Add(new ReleaseEntry("0000000000000000000000000000000000000000", name.ToFileName(), maxfullVer.Filesize)); - - releasesNew.Add(new VelopackAsset { - PackageId = maxfullVer.PackageId, - Version = maxDeltaVer.Version, - Type = VelopackAssetType.Full, - FileName = $"{maxfullVer.PackageId}-{maxDeltaVer.Version}-full.nupkg", - Size = maxfullVer.Filesize, - }); - } + releasesNew.Add(new VelopackAsset { + PackageId = maxfullVer.PackageId, + Version = maxDeltaVer.Version, + Type = VelopackAssetType.Full, + FileName = $"{maxfullVer.PackageId}-{maxDeltaVer.Version}-full.nupkg", + Size = maxfullVer.Filesize, + }); } - - _releasesNew = new VelopackAssetFeed { - Assets = releasesNew.ToArray(), - }; - _releases = releases; } - public Task DownloadBytes(string url, string authorization = null, string accept = null) - { - if (url.Contains($"/{_releasesName}?")) { - MemoryStream ms = new MemoryStream(); - ReleaseEntry.WriteReleaseFile(_releases, ms); - return Task.FromResult(ms.ToArray()); - } + _releasesNew = new VelopackAssetFeed { + Assets = releasesNew.ToArray(), + }; + _releases = releases; + } - if (url.Contains($"/{_releasesNameNew}?")) { - var json = JsonSerializer.Serialize(_releasesNew, SimpleJsonTests.Options); - return Task.FromResult(Encoding.UTF8.GetBytes(json)); - } - - var rel = _releases.FirstOrDefault(r => url.EndsWith(r.OriginalFilename)); - if (rel == null) - throw new Exception("Fake release not found: " + url); - - var filePath = PathHelper.GetFixture(rel.OriginalFilename); - if (!File.Exists(filePath)) { - throw new NotSupportedException("FakeFixtureRepository doesn't have: " + rel.OriginalFilename); - } - - return Task.FromResult(File.ReadAllBytes(filePath)); + public Task DownloadBytes(string url, string authorization = null, string accept = null) + { + if (url.Contains($"/{_releasesName}?")) { + MemoryStream ms = new MemoryStream(); + ReleaseEntry.WriteReleaseFile(_releases, ms); + return Task.FromResult(ms.ToArray()); } - public Task DownloadFile(string url, string targetFile, Action progress, string authorization = null, string accept = null, CancellationToken token = default) - { - var rel = _releases.FirstOrDefault(r => url.EndsWith(r.OriginalFilename)); - var filePath = PathHelper.GetFixture(rel.OriginalFilename); - if (!File.Exists(filePath)) { - throw new NotSupportedException("FakeFixtureRepository doesn't have: " + rel.OriginalFilename); - } - - File.Copy(filePath, targetFile); - progress(25); - progress(50); - progress(75); - progress(100); - return Task.CompletedTask; + if (url.Contains($"/{_releasesNameNew}?")) { + var json = JsonSerializer.Serialize(_releasesNew, SimpleJsonTests.Options); + return Task.FromResult(Encoding.UTF8.GetBytes(json)); } - public Task DownloadString(string url, string authorization = null, string accept = null) - { - if (url.Contains($"/{_releasesName}?")) { - MemoryStream ms = new MemoryStream(); - ReleaseEntry.WriteReleaseFile(_releases, ms); - return Task.FromResult(Encoding.UTF8.GetString(ms.ToArray())); - } + var rel = _releases.FirstOrDefault(r => url.EndsWith(r.OriginalFilename)); + if (rel == null) + throw new Exception("Fake release not found: " + url); - if (url.Contains($"/{_releasesNameNew}?")) { - var json = JsonSerializer.Serialize(_releasesNew, SimpleJsonTests.Options); - return Task.FromResult(json); - } - - throw new NotSupportedException("FakeFixtureRepository doesn't have: " + url); + var filePath = PathHelper.GetFixture(rel.OriginalFilename); + if (!File.Exists(filePath)) { + throw new NotSupportedException("FakeFixtureRepository doesn't have: " + rel.OriginalFilename); } + + return Task.FromResult(File.ReadAllBytes(filePath)); + } + + public Task DownloadFile(string url, string targetFile, Action progress, string authorization = null, string accept = null, CancellationToken token = default) + { + var rel = _releases.FirstOrDefault(r => url.EndsWith(r.OriginalFilename)); + var filePath = PathHelper.GetFixture(rel.OriginalFilename); + if (!File.Exists(filePath)) { + throw new NotSupportedException("FakeFixtureRepository doesn't have: " + rel.OriginalFilename); + } + + File.Copy(filePath, targetFile); + progress(25); + progress(50); + progress(75); + progress(100); + return Task.CompletedTask; + } + + public Task DownloadString(string url, string authorization = null, string accept = null) + { + if (url.Contains($"/{_releasesName}?")) { + MemoryStream ms = new MemoryStream(); + ReleaseEntry.WriteReleaseFile(_releases, ms); + return Task.FromResult(Encoding.UTF8.GetString(ms.ToArray())); + } + + if (url.Contains($"/{_releasesNameNew}?")) { + var json = JsonSerializer.Serialize(_releasesNew, SimpleJsonTests.Options); + return Task.FromResult(json); + } + + throw new NotSupportedException("FakeFixtureRepository doesn't have: " + url); } } diff --git a/test/Velopack.Tests/TestHelpers/StaticHttpServer.cs b/test/Velopack.Tests/TestHelpers/StaticHttpServer.cs index 582a3cea..d3164cb4 100644 --- a/test/Velopack.Tests/TestHelpers/StaticHttpServer.cs +++ b/test/Velopack.Tests/TestHelpers/StaticHttpServer.cs @@ -1,92 +1,91 @@ using System.Net; using System.Text; -namespace Velopack.Tests +namespace Velopack.Tests; + +public sealed class StaticHttpServer : IDisposable { - public sealed class StaticHttpServer : IDisposable + public int Port { get; private set; } + public string RootPath { get; private set; } + + IDisposable inner; + + public StaticHttpServer(int port, string rootPath) { - public int Port { get; private set; } - public string RootPath { get; private set; } + Port = port; RootPath = rootPath; + } - IDisposable inner; - - public StaticHttpServer(int port, string rootPath) - { - Port = port; RootPath = rootPath; + public IDisposable Start() + { + if (inner != null) { + throw new InvalidOperationException("Already started!"); } - public IDisposable Start() - { - if (inner != null) { - throw new InvalidOperationException("Already started!"); - } + var server = new HttpListener(); + server.Prefixes.Add(String.Format("http://+:{0}/", Port)); + server.Start(); - var server = new HttpListener(); - server.Prefixes.Add(String.Format("http://+:{0}/", Port)); - server.Start(); + bool shouldStop = false; + var listener = Task.Run(async () => { + while (!shouldStop) { + var ctx = await server.GetContextAsync(); - bool shouldStop = false; - var listener = Task.Run(async () => { - while (!shouldStop) { - var ctx = await server.GetContextAsync(); - - if (ctx.Request.HttpMethod != "GET") { - closeResponseWith(ctx, 400, "GETs only"); - return; - } - - var target = Path.Combine(RootPath, ctx.Request.Url.AbsolutePath.Replace('/', Path.DirectorySeparatorChar).Substring(1)); - var fi = new FileInfo(target); - - if (!fi.FullName.StartsWith(RootPath)) { - closeResponseWith(ctx, 401, "Not authorized"); - return; - } - - if (!fi.Exists) { - closeResponseWith(ctx, 404, "Not found"); - return; - } - - try { - using (var input = File.OpenRead(target)) { - ctx.Response.StatusCode = 200; - input.CopyTo(ctx.Response.OutputStream); - ctx.Response.Close(); - } - } catch (Exception ex) { - closeResponseWith(ctx, 500, ex.ToString()); - } + if (ctx.Request.HttpMethod != "GET") { + closeResponseWith(ctx, 400, "GETs only"); + return; } - }); - var ret = Disposable.Create(() => { - shouldStop = true; - server.Stop(); - listener.Wait(2000); + var target = Path.Combine(RootPath, ctx.Request.Url.AbsolutePath.Replace('/', Path.DirectorySeparatorChar).Substring(1)); + var fi = new FileInfo(target); - inner = null; - }); + if (!fi.FullName.StartsWith(RootPath)) { + closeResponseWith(ctx, 401, "Not authorized"); + return; + } - inner = ret; - return ret; - } + if (!fi.Exists) { + closeResponseWith(ctx, 404, "Not found"); + return; + } - static void closeResponseWith(HttpListenerContext ctx, int statusCode, string message) - { - ctx.Response.StatusCode = statusCode; - using (var sw = new StreamWriter(ctx.Response.OutputStream, Encoding.UTF8)) { - sw.WriteLine(message); + try { + using (var input = File.OpenRead(target)) { + ctx.Response.StatusCode = 200; + input.CopyTo(ctx.Response.OutputStream); + ctx.Response.Close(); + } + } catch (Exception ex) { + closeResponseWith(ctx, 500, ex.ToString()); + } } - ctx.Response.Close(); - } + }); - public void Dispose() - { - var toDispose = Interlocked.Exchange(ref inner, null); - if (toDispose != null) { - toDispose.Dispose(); - } + var ret = Disposable.Create(() => { + shouldStop = true; + server.Stop(); + listener.Wait(2000); + + inner = null; + }); + + inner = ret; + return ret; + } + + static void closeResponseWith(HttpListenerContext ctx, int statusCode, string message) + { + ctx.Response.StatusCode = statusCode; + using (var sw = new StreamWriter(ctx.Response.OutputStream, Encoding.UTF8)) { + sw.WriteLine(message); + } + ctx.Response.Close(); + } + + public void Dispose() + { + var toDispose = Interlocked.Exchange(ref inner, null); + if (toDispose != null) { + toDispose.Dispose(); } } } diff --git a/test/Velopack.Tests/UpdateManagerTests.cs b/test/Velopack.Tests/UpdateManagerTests.cs index d98e2ccf..c2689325 100644 --- a/test/Velopack.Tests/UpdateManagerTests.cs +++ b/test/Velopack.Tests/UpdateManagerTests.cs @@ -7,378 +7,377 @@ using Velopack.Locators; using Velopack.Sources; using Velopack.Tests.TestHelpers; -namespace Velopack.Tests +namespace Velopack.Tests; + +public class UpdateManagerTests { - public class UpdateManagerTests + private readonly ITestOutputHelper _output; + + public UpdateManagerTests(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; + _output = output; + } - public UpdateManagerTests(ITestOutputHelper output) - { - _output = output; - } + private FakeDownloader GetMockDownloaderNoDelta() + { + var feed = new VelopackAssetFeed() { + Assets = new VelopackAsset[] { + new VelopackAsset() { + PackageId = "MyCoolApp", + Version = new SemanticVersion(1, 1, 0), + Type = VelopackAssetType.Full, + FileName = $"MyCoolApp-1.1.0.nupkg", + SHA1 = "3a2eadd15dd984e4559f2b4d790ec8badaeb6a39", + Size = 1040561, + }, + new VelopackAsset() { + PackageId = "MyCoolApp", + Version = new SemanticVersion(1, 0, 0), + Type = VelopackAssetType.Full, + FileName = $"MyCoolApp-1.0.0.nupkg", + SHA1 = "94689fede03fed7ab59c24337673a27837f0c3ec", + Size = 1004502, + }, + } + }; + var json = JsonSerializer.Serialize(feed, SimpleJsonTests.Options); + return new FakeDownloader() { MockedResponseBytes = Encoding.UTF8.GetBytes(json) }; + } - private FakeDownloader GetMockDownloaderNoDelta() - { - var feed = new VelopackAssetFeed() { - Assets = new VelopackAsset[] { - new VelopackAsset() { - PackageId = "MyCoolApp", - Version = new SemanticVersion(1, 1, 0), - Type = VelopackAssetType.Full, - FileName = $"MyCoolApp-1.1.0.nupkg", - SHA1 = "3a2eadd15dd984e4559f2b4d790ec8badaeb6a39", - Size = 1040561, - }, - new VelopackAsset() { - PackageId = "MyCoolApp", - Version = new SemanticVersion(1, 0, 0), - Type = VelopackAssetType.Full, - FileName = $"MyCoolApp-1.0.0.nupkg", - SHA1 = "94689fede03fed7ab59c24337673a27837f0c3ec", - Size = 1004502, - }, - } - }; - var json = JsonSerializer.Serialize(feed, SimpleJsonTests.Options); - return new FakeDownloader() { MockedResponseBytes = Encoding.UTF8.GetBytes(json) }; - } + private FakeDownloader GetMockDownloaderWith2Delta() + { + var feed = new VelopackAssetFeed { + Assets = new VelopackAsset[] { + new VelopackAsset() { + PackageId = "MyCoolApp", + Version = new SemanticVersion(1, 1, 0), + Type = VelopackAssetType.Full, + FileName = $"MyCoolApp-1.1.0.nupkg", + SHA1 = "3a2eadd15dd984e4559f2b4d790ec8badaeb6a39", + Size = 1040561, + }, + new VelopackAsset() { + PackageId = "MyCoolApp", + Version = new SemanticVersion(1, 0, 0), + Type = VelopackAssetType.Full, + FileName = $"MyCoolApp-1.0.0.nupkg", + SHA1 = "94689fede03fed7ab59c24337673a27837f0c3ec", + Size = 1004502, + }, + new VelopackAsset() { + PackageId = "MyCoolApp", + Version = new SemanticVersion(1, 1, 0), + Type = VelopackAssetType.Delta, + FileName = $"MyCoolApp-1.1.0-delta.nupkg", + SHA1 = "14db31d2647c6d2284882a2e101924a9c409ee67", + Size = 80396, + }, + new VelopackAsset() { + PackageId = "MyCoolApp", + Version = new SemanticVersion(1, 0, 0), + Type = VelopackAssetType.Delta, + FileName = $"MyCoolApp-1.0.0-delta.nupkg", + SHA1 = "14db31d2647c6d2284882a2e101924a9c409ee67", + Size = 80396, + }, + new VelopackAsset() { + PackageId = "MyCoolApp", + Version = new SemanticVersion(1, 2, 0), + Type = VelopackAssetType.Delta, + FileName = $"MyCoolApp-1.2.0-delta.nupkg", + SHA1 = "14db31d2647c6d2284882a2e101924a9c409ee67", + Size = 80396, + }, + new VelopackAsset() { + PackageId = "MyCoolApp", + Version = new SemanticVersion(1, 2, 0), + Type = VelopackAssetType.Full, + FileName = $"MyCoolApp-1.2.0.nupkg", + SHA1 = "3a2eadd15dd984e4559f2b4d790ec8badaeb6a39", + Size = 1040561, + }, + } + }; + var json = JsonSerializer.Serialize(feed, SimpleJsonTests.Options); + return new FakeDownloader() { MockedResponseBytes = Encoding.UTF8.GetBytes(json) }; + } - private FakeDownloader GetMockDownloaderWith2Delta() - { - var feed = new VelopackAssetFeed { - Assets = new VelopackAsset[] { - new VelopackAsset() { - PackageId = "MyCoolApp", - Version = new SemanticVersion(1, 1, 0), - Type = VelopackAssetType.Full, - FileName = $"MyCoolApp-1.1.0.nupkg", - SHA1 = "3a2eadd15dd984e4559f2b4d790ec8badaeb6a39", - Size = 1040561, - }, - new VelopackAsset() { - PackageId = "MyCoolApp", - Version = new SemanticVersion(1, 0, 0), - Type = VelopackAssetType.Full, - FileName = $"MyCoolApp-1.0.0.nupkg", - SHA1 = "94689fede03fed7ab59c24337673a27837f0c3ec", - Size = 1004502, - }, - new VelopackAsset() { - PackageId = "MyCoolApp", - Version = new SemanticVersion(1, 1, 0), - Type = VelopackAssetType.Delta, - FileName = $"MyCoolApp-1.1.0-delta.nupkg", - SHA1 = "14db31d2647c6d2284882a2e101924a9c409ee67", - Size = 80396, - }, - new VelopackAsset() { - PackageId = "MyCoolApp", - Version = new SemanticVersion(1, 0, 0), - Type = VelopackAssetType.Delta, - FileName = $"MyCoolApp-1.0.0-delta.nupkg", - SHA1 = "14db31d2647c6d2284882a2e101924a9c409ee67", - Size = 80396, - }, - new VelopackAsset() { - PackageId = "MyCoolApp", - Version = new SemanticVersion(1, 2, 0), - Type = VelopackAssetType.Delta, - FileName = $"MyCoolApp-1.2.0-delta.nupkg", - SHA1 = "14db31d2647c6d2284882a2e101924a9c409ee67", - Size = 80396, - }, - new VelopackAsset() { - PackageId = "MyCoolApp", - Version = new SemanticVersion(1, 2, 0), - Type = VelopackAssetType.Full, - FileName = $"MyCoolApp-1.2.0.nupkg", - SHA1 = "3a2eadd15dd984e4559f2b4d790ec8badaeb6a39", - Size = 1040561, - }, - } - }; - var json = JsonSerializer.Serialize(feed, SimpleJsonTests.Options); - return new FakeDownloader() { MockedResponseBytes = Encoding.UTF8.GetBytes(json) }; - } + [Fact] + public void CheckFromLocal() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var tempPath); + var dl = GetMockDownloaderNoDelta(); + var source = new SimpleWebSource("http://any.com", dl); + var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger); + var um = new UpdateManager(source, null, logger, locator); + var info = um.CheckForUpdates(); + Assert.NotNull(info); + Assert.True(new SemanticVersion(1, 1, 0) == info.TargetFullRelease.Version); + Assert.Equal(0, info.DeltasToTarget.Count()); + Assert.False(info.IsDowngrade); + Assert.StartsWith($"http://any.com/releases.{VelopackRuntimeInfo.SystemOs.GetOsShortName()}.json?", dl.LastUrl); + } - [Fact] - public void CheckFromLocal() - { - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var tempPath); - var dl = GetMockDownloaderNoDelta(); - var source = new SimpleWebSource("http://any.com", dl); - var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger); - var um = new UpdateManager(source, null, logger, locator); - var info = um.CheckForUpdates(); - Assert.NotNull(info); - Assert.True(new SemanticVersion(1, 1, 0) == info.TargetFullRelease.Version); - Assert.Equal(0, info.DeltasToTarget.Count()); - Assert.False(info.IsDowngrade); - Assert.StartsWith($"http://any.com/releases.{VelopackRuntimeInfo.SystemOs.GetOsShortName()}.json?", dl.LastUrl); - } + [Fact] + public void CheckFromLocalWithChannel() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var tempPath); + var dl = GetMockDownloaderNoDelta(); + var source = new SimpleWebSource("http://any.com", dl); + var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger); + var opt = new UpdateOptions { ExplicitChannel = "experimental" }; + var um = new UpdateManager(source, opt, logger, locator); + var info = um.CheckForUpdates(); + Assert.NotNull(info); + Assert.True(new SemanticVersion(1, 1, 0) == info.TargetFullRelease.Version); + Assert.Equal(0, info.DeltasToTarget.Count()); + Assert.False(info.IsDowngrade); + Assert.StartsWith("http://any.com/releases.experimental.json?", dl.LastUrl); + } - [Fact] - public void CheckFromLocalWithChannel() - { - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var tempPath); - var dl = GetMockDownloaderNoDelta(); - var source = new SimpleWebSource("http://any.com", dl); - var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger); - var opt = new UpdateOptions { ExplicitChannel = "experimental" }; - var um = new UpdateManager(source, opt, logger, locator); - var info = um.CheckForUpdates(); - Assert.NotNull(info); - Assert.True(new SemanticVersion(1, 1, 0) == info.TargetFullRelease.Version); - Assert.Equal(0, info.DeltasToTarget.Count()); - Assert.False(info.IsDowngrade); - Assert.StartsWith("http://any.com/releases.experimental.json?", dl.LastUrl); - } + [Fact] + public void CheckForSameAsInstalledVersion() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var tempPath); + var dl = GetMockDownloaderWith2Delta(); + var myVer = new VelopackAsset() { + PackageId = "MyCoolApp", + Version = new SemanticVersion(1, 2, 0), + Type = VelopackAssetType.Full, + FileName = $"MyCoolApp-1.2.0.nupkg", + SHA1 = "3a2eadd15dd984e4559f2b4d790ec8badaeb6a39", + Size = 1040561, + }; + var source = new SimpleWebSource("http://any.com", dl); + var locator = new TestVelopackLocator("MyCoolApp", "1.2.0", tempPath, null, null, null, logger: logger, localPackage: myVer, channel: "stable"); - [Fact] - public void CheckForSameAsInstalledVersion() - { - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var tempPath); - var dl = GetMockDownloaderWith2Delta(); - var myVer = new VelopackAsset() { - PackageId = "MyCoolApp", - Version = new SemanticVersion(1, 2, 0), - Type = VelopackAssetType.Full, - FileName = $"MyCoolApp-1.2.0.nupkg", - SHA1 = "3a2eadd15dd984e4559f2b4d790ec8badaeb6a39", - Size = 1040561, - }; - var source = new SimpleWebSource("http://any.com", dl); - var locator = new TestVelopackLocator("MyCoolApp", "1.2.0", tempPath, null, null, null, logger: logger, localPackage: myVer, channel: "stable"); + // checking for same version should return null + var um = new UpdateManager(source, null, logger, locator); + var info = um.CheckForUpdates(); + Assert.Null(info); + Assert.StartsWith("http://any.com/releases.stable.json?", dl.LastUrl); - // checking for same version should return null - var um = new UpdateManager(source, null, logger, locator); - var info = um.CheckForUpdates(); - Assert.Null(info); - Assert.StartsWith("http://any.com/releases.stable.json?", dl.LastUrl); + // checking for same version WITHOUT explicit channel should return null + var opt = new UpdateOptions { AllowVersionDowngrade = true }; + um = new UpdateManager(source, opt, logger, locator); + Assert.Null(info); + Assert.StartsWith("http://any.com/releases.stable.json?", dl.LastUrl); - // checking for same version WITHOUT explicit channel should return null - var opt = new UpdateOptions { AllowVersionDowngrade = true }; - um = new UpdateManager(source, opt, logger, locator); - Assert.Null(info); - Assert.StartsWith("http://any.com/releases.stable.json?", dl.LastUrl); + // checking for same version with explicit channel & downgrade allowed should return version + opt = new UpdateOptions { ExplicitChannel = "experimental", AllowVersionDowngrade = true }; + um = new UpdateManager(source, opt, logger, locator); + info = um.CheckForUpdates(); + Assert.True(info.IsDowngrade); + Assert.NotNull(info); + Assert.True(new SemanticVersion(1, 2, 0) == info.TargetFullRelease.Version); + Assert.StartsWith("http://any.com/releases.experimental.json?", dl.LastUrl); + Assert.Equal(0, info.DeltasToTarget.Count()); + } - // checking for same version with explicit channel & downgrade allowed should return version - opt = new UpdateOptions { ExplicitChannel = "experimental", AllowVersionDowngrade = true }; - um = new UpdateManager(source, opt, logger, locator); - info = um.CheckForUpdates(); - Assert.True(info.IsDowngrade); - Assert.NotNull(info); - Assert.True(new SemanticVersion(1, 2, 0) == info.TargetFullRelease.Version); - Assert.StartsWith("http://any.com/releases.experimental.json?", dl.LastUrl); - Assert.Equal(0, info.DeltasToTarget.Count()); - } + [Fact] + public void CheckForLowerThanInstalledVersion() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var tempPath); + var dl = GetMockDownloaderWith2Delta(); + var myVer = new VelopackAsset() { + PackageId = "MyCoolApp", + Version = new SemanticVersion(2, 0, 0), + Type = VelopackAssetType.Full, + FileName = $"MyCoolApp-2.0.0.nupkg", + SHA1 = "3a2eadd15dd984e4559f2b4d790ec8badaeb6a39", + Size = 1040561, + }; + var source = new SimpleWebSource("http://any.com", dl); + var locator = new TestVelopackLocator("MyCoolApp", "2.0.0", tempPath, null, null, null, logger: logger, localPackage: myVer, channel: "stable"); - [Fact] - public void CheckForLowerThanInstalledVersion() - { - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var tempPath); - var dl = GetMockDownloaderWith2Delta(); - var myVer = new VelopackAsset() { - PackageId = "MyCoolApp", - Version = new SemanticVersion(2, 0, 0), - Type = VelopackAssetType.Full, - FileName = $"MyCoolApp-2.0.0.nupkg", - SHA1 = "3a2eadd15dd984e4559f2b4d790ec8badaeb6a39", - Size = 1040561, - }; - var source = new SimpleWebSource("http://any.com", dl); - var locator = new TestVelopackLocator("MyCoolApp", "2.0.0", tempPath, null, null, null, logger: logger, localPackage: myVer, channel: "stable"); + // checking for lower version should return null + var um = new UpdateManager(source, null, logger, locator); + var info = um.CheckForUpdates(); + Assert.Null(info); + Assert.StartsWith("http://any.com/releases.stable.json?", dl.LastUrl); - // checking for lower version should return null - var um = new UpdateManager(source, null, logger, locator); - var info = um.CheckForUpdates(); - Assert.Null(info); - Assert.StartsWith("http://any.com/releases.stable.json?", dl.LastUrl); + // checking for lower version with downgrade allowed should return lower version + var opt = new UpdateOptions { AllowVersionDowngrade = true }; + um = new UpdateManager(source, opt, logger, locator); + info = um.CheckForUpdates(); + Assert.True(info.IsDowngrade); + Assert.NotNull(info); + Assert.True(new SemanticVersion(1, 2, 0) == info.TargetFullRelease.Version); + Assert.StartsWith("http://any.com/releases.stable.json?", dl.LastUrl); + Assert.Equal(0, info.DeltasToTarget.Count()); + } - // checking for lower version with downgrade allowed should return lower version - var opt = new UpdateOptions { AllowVersionDowngrade = true }; - um = new UpdateManager(source, opt, logger, locator); - info = um.CheckForUpdates(); - Assert.True(info.IsDowngrade); - Assert.NotNull(info); - Assert.True(new SemanticVersion(1, 2, 0) == info.TargetFullRelease.Version); - Assert.StartsWith("http://any.com/releases.stable.json?", dl.LastUrl); - Assert.Equal(0, info.DeltasToTarget.Count()); - } + [Fact] + public void CheckFromLocalWithDelta() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var tempPath); + var dl = GetMockDownloaderWith2Delta(); + var myVer = new VelopackAsset() { + PackageId = "MyCoolApp", + Version = new SemanticVersion(1, 0, 0), + Type = VelopackAssetType.Full, + FileName = $"MyCoolApp-1.0.0.nupkg", + SHA1 = "94689fede03fed7ab59c24337673a27837f0c3ec", + Size = 1004502, + }; + var source = new SimpleWebSource("http://any.com", dl); - [Fact] - public void CheckFromLocalWithDelta() - { - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var tempPath); - var dl = GetMockDownloaderWith2Delta(); - var myVer = new VelopackAsset() { - PackageId = "MyCoolApp", - Version = new SemanticVersion(1, 0, 0), - Type = VelopackAssetType.Full, - FileName = $"MyCoolApp-1.0.0.nupkg", - SHA1 = "94689fede03fed7ab59c24337673a27837f0c3ec", - Size = 1004502, - }; - var source = new SimpleWebSource("http://any.com", dl); + var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, null, null, null, logger: logger, localPackage: myVer); + var um = new UpdateManager(source, null, logger, locator); + var info = um.CheckForUpdates(); + Assert.False(info.IsDowngrade); + Assert.NotNull(info); + Assert.True(new SemanticVersion(1, 2, 0) == info.TargetFullRelease.Version); + Assert.Equal(2, info.DeltasToTarget.Count()); + } - var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, null, null, null, logger: logger, localPackage: myVer); - var um = new UpdateManager(source, null, logger, locator); - var info = um.CheckForUpdates(); - Assert.False(info.IsDowngrade); - Assert.NotNull(info); - Assert.True(new SemanticVersion(1, 2, 0) == info.TargetFullRelease.Version); - Assert.Equal(2, info.DeltasToTarget.Count()); - } + [Fact] + public void NoDeltaIfNoBasePackage() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var tempPath); + var dl = GetMockDownloaderWith2Delta(); + var source = new SimpleWebSource("http://any.com", dl); + var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger: logger); + var um = new UpdateManager(source, null, logger, locator); + var info = um.CheckForUpdates(); + Assert.NotNull(info); + Assert.False(info.IsDowngrade); + Assert.True(new SemanticVersion(1, 2, 0) == info.TargetFullRelease.Version); + Assert.Equal(0, info.DeltasToTarget.Count()); + } - [Fact] - public void NoDeltaIfNoBasePackage() - { - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var tempPath); - var dl = GetMockDownloaderWith2Delta(); - var source = new SimpleWebSource("http://any.com", dl); - var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger: logger); - var um = new UpdateManager(source, null, logger, locator); - var info = um.CheckForUpdates(); - Assert.NotNull(info); - Assert.False(info.IsDowngrade); - Assert.True(new SemanticVersion(1, 2, 0) == info.TargetFullRelease.Version); - Assert.Equal(0, info.DeltasToTarget.Count()); - } + [Fact] + public void CheckFromLocalWithDeltaNoLocalPackage() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var tempPath); + var dl = GetMockDownloaderWith2Delta(); + var source = new SimpleWebSource("http://any.com", dl); + var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger: logger); + var um = new UpdateManager(source, null, logger, locator); + var info = um.CheckForUpdates(); + Assert.NotNull(info); + Assert.False(info.IsDowngrade); + Assert.True(new SemanticVersion(1, 2, 0) == info.TargetFullRelease.Version); + Assert.Equal(0, info.DeltasToTarget.Count()); + } - [Fact] - public void CheckFromLocalWithDeltaNoLocalPackage() - { - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var tempPath); - var dl = GetMockDownloaderWith2Delta(); - var source = new SimpleWebSource("http://any.com", dl); - var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger: logger); - var um = new UpdateManager(source, null, logger, locator); - var info = um.CheckForUpdates(); - Assert.NotNull(info); - Assert.False(info.IsDowngrade); - Assert.True(new SemanticVersion(1, 2, 0) == info.TargetFullRelease.Version); - Assert.Equal(0, info.DeltasToTarget.Count()); - } + [Fact(Skip = "Consumes API Quota")] + public void CheckGithub() + { + // https://github.com/caesay/SquirrelCustomLauncherTestApp + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var tempPath); + var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger); + var source = new GithubSource("https://github.com/caesay/SquirrelCustomLauncherTestApp", null, false); + var um = new UpdateManager(source, null, logger, locator); + var info = um.CheckForUpdates(); + Assert.NotNull(info); + Assert.True(new SemanticVersion(1, 0, 1) == info.TargetFullRelease.Version); + Assert.Equal(0, info.DeltasToTarget.Count()); + } - [Fact(Skip = "Consumes API Quota")] - public void CheckGithub() - { - // https://github.com/caesay/SquirrelCustomLauncherTestApp - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var tempPath); - var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger); - var source = new GithubSource("https://github.com/caesay/SquirrelCustomLauncherTestApp", null, false); - var um = new UpdateManager(source, null, logger, locator); - var info = um.CheckForUpdates(); - Assert.NotNull(info); - Assert.True(new SemanticVersion(1, 0, 1) == info.TargetFullRelease.Version); - Assert.Equal(0, info.DeltasToTarget.Count()); - } + [Fact(Skip = "Consumes API Quota")] + public void CheckGithubWithNonExistingChannel() + { + // https://github.com/caesay/SquirrelCustomLauncherTestApp + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var tempPath); + var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger); + var source = new GithubSource("https://github.com/caesay/SquirrelCustomLauncherTestApp", null, false); + var opt = new UpdateOptions { ExplicitChannel = "hello" }; + var um = new UpdateManager(source, opt, logger, locator); + Assert.Throws(() => um.CheckForUpdates()); + } - [Fact(Skip = "Consumes API Quota")] - public void CheckGithubWithNonExistingChannel() - { - // https://github.com/caesay/SquirrelCustomLauncherTestApp - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var tempPath); - var locator = new TestVelopackLocator("MyCoolApp", "1.0.0", tempPath, logger); - var source = new GithubSource("https://github.com/caesay/SquirrelCustomLauncherTestApp", null, false); - var opt = new UpdateOptions { ExplicitChannel = "hello" }; - var um = new UpdateManager(source, opt, logger, locator); - Assert.Throws(() => um.CheckForUpdates()); - } + [Fact] + public void NoUpdatesIfCurrentEqualsRemoteVersion() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var tempPath); + var dl = GetMockDownloaderNoDelta(); + var source = new SimpleWebSource("http://any.com", dl); + var locator = new TestVelopackLocator("MyCoolApp", "1.1.0", tempPath, logger); + var um = new UpdateManager(source, null, logger, locator); + var info = um.CheckForUpdates(); + Assert.Null(info); + } - [Fact] - public void NoUpdatesIfCurrentEqualsRemoteVersion() - { - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var tempPath); - var dl = GetMockDownloaderNoDelta(); - var source = new SimpleWebSource("http://any.com", dl); - var locator = new TestVelopackLocator("MyCoolApp", "1.1.0", tempPath, logger); - var um = new UpdateManager(source, null, logger, locator); - var info = um.CheckForUpdates(); - Assert.Null(info); - } + [Fact] + public void NoUpdatesIfCurrentGreaterThanRemoteVersion() + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var tempPath); + var dl = GetMockDownloaderNoDelta(); + var source = new SimpleWebSource("http://any.com", dl); + var locator = new TestVelopackLocator("MyCoolApp", "1.2.0", tempPath, logger); + var um = new UpdateManager(source, null, logger, locator); + var info = um.CheckForUpdates(); + Assert.Null(info); + } - [Fact] - public void NoUpdatesIfCurrentGreaterThanRemoteVersion() - { - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var tempPath); - var dl = GetMockDownloaderNoDelta(); - var source = new SimpleWebSource("http://any.com", dl); - var locator = new TestVelopackLocator("MyCoolApp", "1.2.0", tempPath, logger); - var um = new UpdateManager(source, null, logger, locator); - var info = um.CheckForUpdates(); - Assert.Null(info); - } + [Theory] + [InlineData("Clowd", "3.4.287")] + [InlineData("slack", "1.1.8")] + public void DownloadsLatestFullVersion(string id, string version) + { + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var packagesDir); + var repo = new FakeFixtureRepository(id, false); + var source = new SimpleWebSource("http://any.com", repo); + var locator = new TestVelopackLocator(id, "1.0.0", packagesDir, logger); + var um = new UpdateManager(source, null, logger, locator); - [Theory] - [InlineData("Clowd", "3.4.287")] - [InlineData("slack", "1.1.8")] - public void DownloadsLatestFullVersion(string id, string version) - { - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var packagesDir); - var repo = new FakeFixtureRepository(id, false); - var source = new SimpleWebSource("http://any.com", repo); - var locator = new TestVelopackLocator(id, "1.0.0", packagesDir, logger); - var um = new UpdateManager(source, null, logger, locator); + var info = um.CheckForUpdates(); + Assert.NotNull(info); + Assert.True(SemanticVersion.Parse(version) == info.TargetFullRelease.Version); + Assert.Equal(0, info.DeltasToTarget.Count()); - var info = um.CheckForUpdates(); - Assert.NotNull(info); - Assert.True(SemanticVersion.Parse(version) == info.TargetFullRelease.Version); - Assert.Equal(0, info.DeltasToTarget.Count()); + um.DownloadUpdates(info); - um.DownloadUpdates(info); + var target = Path.Combine(packagesDir, $"{id}-{version}-full.nupkg"); + Assert.True(File.Exists(target)); + um.VerifyPackageChecksum(info.TargetFullRelease); + } - var target = Path.Combine(packagesDir, $"{id}-{version}-full.nupkg"); - Assert.True(File.Exists(target)); - um.VerifyPackageChecksum(info.TargetFullRelease); - } + [SkippableTheory] + [InlineData("Clowd", "3.4.287", "3.4.292")] + //[InlineData("slack", "1.1.8", "1.2.2")] + public async Task DownloadsDeltasAndCreatesFullVersion(string id, string fromVersion, string toVersion) + { + Skip.If(VelopackRuntimeInfo.IsLinux); + using var logger = _output.BuildLoggerFor(); + using var _1 = Utility.GetTempDirectory(out var packagesDir); + var repo = new FakeFixtureRepository(id, true); + var source = new SimpleWebSource("http://any.com", repo); - [SkippableTheory] - [InlineData("Clowd", "3.4.287", "3.4.292")] - //[InlineData("slack", "1.1.8", "1.2.2")] - public async Task DownloadsDeltasAndCreatesFullVersion(string id, string fromVersion, string toVersion) - { - Skip.If(VelopackRuntimeInfo.IsLinux); - using var logger = _output.BuildLoggerFor(); - using var _1 = Utility.GetTempDirectory(out var packagesDir); - var repo = new FakeFixtureRepository(id, true); - var source = new SimpleWebSource("http://any.com", repo); + var feed = await source.GetReleaseFeed(logger, VelopackRuntimeInfo.SystemOs.GetOsShortName()); + var basePkg = feed.Assets + .Where(x => x.Type == VelopackAssetType.Full) + .Single(x => x.Version == SemanticVersion.Parse(fromVersion)); + var basePkgFixturePath = PathHelper.GetFixture(basePkg.FileName); + var basePkgPath = Path.Combine(packagesDir, basePkg.FileName); + File.Copy(basePkgFixturePath, basePkgPath); - var feed = await source.GetReleaseFeed(logger, VelopackRuntimeInfo.SystemOs.GetOsShortName()); - var basePkg = feed.Assets - .Where(x => x.Type == VelopackAssetType.Full) - .Single(x => x.Version == SemanticVersion.Parse(fromVersion)); - var basePkgFixturePath = PathHelper.GetFixture(basePkg.FileName); - var basePkgPath = Path.Combine(packagesDir, basePkg.FileName); - File.Copy(basePkgFixturePath, basePkgPath); + var updateExe = PathHelper.CopyUpdateTo(packagesDir); + var locator = new TestVelopackLocator(id, fromVersion, + packagesDir, null, null, updateExe, null, logger); + var um = new UpdateManager(source, null, logger, locator); - var updateExe = PathHelper.CopyUpdateTo(packagesDir); - var locator = new TestVelopackLocator(id, fromVersion, - packagesDir, null, null, updateExe, null, logger); - var um = new UpdateManager(source, null, logger, locator); + var info = await um.CheckForUpdatesAsync(); + Assert.NotNull(info); + Assert.True(SemanticVersion.Parse(toVersion) == info.TargetFullRelease.Version); + Assert.Equal(3, info.DeltasToTarget.Count()); + Assert.NotNull(info.BaseRelease); - var info = await um.CheckForUpdatesAsync(); - Assert.NotNull(info); - Assert.True(SemanticVersion.Parse(toVersion) == info.TargetFullRelease.Version); - Assert.Equal(3, info.DeltasToTarget.Count()); - Assert.NotNull(info.BaseRelease); - - await um.DownloadUpdatesAsync(info); - var target = Path.Combine(packagesDir, $"{id}-{toVersion}-full.nupkg"); - Assert.True(File.Exists(target)); - } + await um.DownloadUpdatesAsync(info); + var target = Path.Combine(packagesDir, $"{id}-{toVersion}-full.nupkg"); + Assert.True(File.Exists(target)); } } diff --git a/test/Velopack.Tests/UtilityTests.cs b/test/Velopack.Tests/UtilityTests.cs index 06e2bb41..a979a383 100644 --- a/test/Velopack.Tests/UtilityTests.cs +++ b/test/Velopack.Tests/UtilityTests.cs @@ -4,235 +4,234 @@ using System.Security.Cryptography; using System.Text; using Velopack.Windows; -namespace Velopack.Tests +namespace Velopack.Tests; + +public class UtilityTests { - public class UtilityTests + private readonly ITestOutputHelper _output; + + public UtilityTests(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; - - public UtilityTests(ITestOutputHelper output) - { - _output = output; - } - - [SkippableTheory] - [InlineData("file.txt", "file.txt")] - [InlineData("file", "file")] - [InlineData("/file", "\\file")] - [InlineData("/file/", "\\file")] - [InlineData("one\\two\\..\\file", "one\\file")] - [InlineData("C:/AnApp/file/", "C:\\AnApp\\file")] - public void PathIsNormalized(string input, string expected) - { - Skip.IfNot(VelopackRuntimeInfo.IsWindows); - var exp = Path.GetFullPath(expected); - var normal = Utility.NormalizePath(input); - Assert.Equal(exp, normal); - } - - [SkippableTheory] - [InlineData("C:\\AnApp", "C:\\AnApp\\file.exe", true)] - [InlineData("C:\\AnApp\\", "C:\\AnApp\\file.exe", true)] - [InlineData("C:\\AnApp", "C:\\AnApp\\sub\\dir\\file.exe", true)] - [InlineData("C:\\AnApp\\", "C:\\AnApp\\sub\\dir\\file.exe", true)] - [InlineData("C:\\AnAppTwo", "C:\\AnApp\\file.exe", false)] - [InlineData("C:\\AnAppTwo\\", "C:\\AnApp\\file.exe", false)] - [InlineData("C:\\AnAppTwo", "C:\\AnApp\\sub\\dir\\file.exe", false)] - [InlineData("C:\\AnAppTwo\\", "C:\\AnApp\\sub\\dir\\file.exe", false)] - [InlineData("AnAppThree", "AnAppThree\\file.exe", true)] - public void FileIsInDirectory(string directory, string file, bool isIn) - { - Skip.IfNot(VelopackRuntimeInfo.IsWindows); - var fileInDir = Utility.IsFileInDirectory(file, directory); - Assert.Equal(isIn, fileInDir); - } - - [SkippableFact] - [SupportedOSPlatform("windows")] - public void SetAppIdOnShortcutTest() - { - Skip.IfNot(VelopackRuntimeInfo.IsWindows); - var sl = new ShellLink() { - Target = @"C:\Windows\Notepad.exe", - Description = "It's Notepad", - }; - - sl.SetAppUserModelId("org.anaïsbetts.test"); - var path = Path.GetFullPath(@".\test.lnk"); - sl.Save(path); - - Console.WriteLine("Saved to " + path); - } - - [Fact] - public void RemoveByteOrderMarkerIfPresent() - { - var utf32Be = new byte[] { 0x00, 0x00, 0xFE, 0xFF }; - var utf32Le = new byte[] { 0xFF, 0xFE, 0x00, 0x00 }; - var utf16Be = new byte[] { 0xFE, 0xFF }; - var utf16Le = new byte[] { 0xFF, 0xFE }; - var utf8 = new byte[] { 0xEF, 0xBB, 0xBF }; - - var utf32BeHelloWorld = combine(utf32Be, Encoding.UTF8.GetBytes("hello world")); - var utf32LeHelloWorld = combine(utf32Le, Encoding.UTF8.GetBytes("hello world")); - var utf16BeHelloWorld = combine(utf16Be, Encoding.UTF8.GetBytes("hello world")); - var utf16LeHelloWorld = combine(utf16Le, Encoding.UTF8.GetBytes("hello world")); - var utf8HelloWorld = combine(utf8, Encoding.UTF8.GetBytes("hello world")); - - var asciiMultipleChars = Encoding.ASCII.GetBytes("hello world"); - var asciiSingleChar = Encoding.ASCII.GetBytes("A"); - - var emptyString = string.Empty; - string nullString = null; - byte[] nullByteArray = { }; - Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(emptyString)); - Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(nullString)); - Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(nullByteArray)); - - Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(utf32Be)); - Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(utf32Le)); - Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(utf16Be)); - Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(utf16Le)); - Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(utf8)); - - Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(utf32BeHelloWorld)); - Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(utf32LeHelloWorld)); - Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(utf16BeHelloWorld)); - Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(utf16LeHelloWorld)); - Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(utf8HelloWorld)); - - Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(asciiMultipleChars)); - Assert.Equal("A", Utility.RemoveByteOrderMarkerIfPresent(asciiSingleChar)); - } - - [Fact] - public void ShaCheckShouldBeCaseInsensitive() - { - var sha1FromExternalTool = "75255cfd229a1ed1447abe1104f5635e69975d30"; - var inputPackage = PathHelper.GetFixture("Squirrel.Core.1.0.0.0.nupkg"); - var stream = File.OpenRead(inputPackage); - var sha1 = Utility.CalculateStreamSHA1(stream); - - Assert.NotEqual(sha1FromExternalTool, sha1); - Assert.Equal(sha1FromExternalTool, sha1, StringComparer.OrdinalIgnoreCase); - } - - [Fact] - public void CanDeleteDeepRecursiveDirectoryStructure() - { - using var logger = _output.BuildLoggerFor(); - string tempDir; - using (Utility.GetTempDirectory(out tempDir)) { - for (var i = 0; i < 50; i++) { - var directory = Path.Combine(tempDir, newId()); - CreateSampleDirectory(directory); - } - - var files = Directory.GetFiles(tempDir, "*", SearchOption.AllDirectories); - - var count = files.Count(); - - logger.Info($"Created {count} files under directory {tempDir}"); - - var sw = new Stopwatch(); - sw.Start(); - Utility.DeleteFileOrDirectoryHard(tempDir); - sw.Stop(); - logger.Info($"Delete took {sw.ElapsedMilliseconds}ms"); - - Assert.False(Directory.Exists(tempDir)); - } - } - - //[Fact] - //public void CreateFakePackageSmokeTest() - //{ - // string path; - // using (Utility.GetTempDirectory(out path)) { - // var output = IntegrationTestHelper.CreateFakeInstalledApp("0.3.0", path); - // Assert.True(File.Exists(output)); - // } - //} - - [Theory] - [InlineData("foo.dll", true)] - [InlineData("foo.DlL", true)] - [InlineData("C:\\Foo\\Bar\\foo.Exe", true)] - [InlineData("Test.png", false)] - [InlineData(".rels", false)] - public void FileIsLikelyPEImageTest(string input, bool result) - { - Assert.Equal(result, Utility.FileIsLikelyPEImage(input)); - } - - [Fact(Skip = "Only really need to run this test after changes to FileDownloader")] - public async Task DownloaderReportsProgress() - { - // this probably should use a local http server instead. - const string testUrl = "http://speedtest.tele2.net/1MB.zip"; - - var dl = Utility.CreateDefaultDownloader(); - - List prog = new List(); - using (Utility.GetTempFileName(out var tempPath)) - await dl.DownloadFile(testUrl, tempPath, prog.Add); - - Assert.True(prog.Count > 10); - Assert.Equal(100, prog.Last()); - Assert.True(prog[1] != 0); - } - - static void CreateSampleDirectory(string directory) - { - Random prng = new Random(); - while (true) { - Directory.CreateDirectory(directory); - - for (var j = 0; j < 100; j++) { - var file = Path.Combine(directory, newId()); - if (file.Length > 260) continue; - File.WriteAllText(file, Guid.NewGuid().ToString()); - } - - if (prng.NextDouble() > 0.5) { - var childDirectory = Path.Combine(directory, newId()); - if (childDirectory.Length > 248) return; - directory = childDirectory; - continue; - } - - break; - } - } - - static string newId() - { - var text = Guid.NewGuid().ToString(); - var bytes = Encoding.Unicode.GetBytes(text); - var provider = SHA1.Create(); - var hashString = string.Empty; - - foreach (var x in provider.ComputeHash(bytes)) { - hashString += String.Format("{0:x2}", x); - } - - if (hashString.Length > 7) { - return hashString.Substring(0, 7); - } - - return hashString; - } - - static byte[] combine(params byte[][] arrays) - { - var rv = new byte[arrays.Sum(a => a.Length)]; - var offset = 0; - foreach (var array in arrays) { - Buffer.BlockCopy(array, 0, rv, offset, array.Length); - offset += array.Length; - } - return rv; - } - + _output = output; } + + [SkippableTheory] + [InlineData("file.txt", "file.txt")] + [InlineData("file", "file")] + [InlineData("/file", "\\file")] + [InlineData("/file/", "\\file")] + [InlineData("one\\two\\..\\file", "one\\file")] + [InlineData("C:/AnApp/file/", "C:\\AnApp\\file")] + public void PathIsNormalized(string input, string expected) + { + Skip.IfNot(VelopackRuntimeInfo.IsWindows); + var exp = Path.GetFullPath(expected); + var normal = Utility.NormalizePath(input); + Assert.Equal(exp, normal); + } + + [SkippableTheory] + [InlineData("C:\\AnApp", "C:\\AnApp\\file.exe", true)] + [InlineData("C:\\AnApp\\", "C:\\AnApp\\file.exe", true)] + [InlineData("C:\\AnApp", "C:\\AnApp\\sub\\dir\\file.exe", true)] + [InlineData("C:\\AnApp\\", "C:\\AnApp\\sub\\dir\\file.exe", true)] + [InlineData("C:\\AnAppTwo", "C:\\AnApp\\file.exe", false)] + [InlineData("C:\\AnAppTwo\\", "C:\\AnApp\\file.exe", false)] + [InlineData("C:\\AnAppTwo", "C:\\AnApp\\sub\\dir\\file.exe", false)] + [InlineData("C:\\AnAppTwo\\", "C:\\AnApp\\sub\\dir\\file.exe", false)] + [InlineData("AnAppThree", "AnAppThree\\file.exe", true)] + public void FileIsInDirectory(string directory, string file, bool isIn) + { + Skip.IfNot(VelopackRuntimeInfo.IsWindows); + var fileInDir = Utility.IsFileInDirectory(file, directory); + Assert.Equal(isIn, fileInDir); + } + + [SkippableFact] + [SupportedOSPlatform("windows")] + public void SetAppIdOnShortcutTest() + { + Skip.IfNot(VelopackRuntimeInfo.IsWindows); + var sl = new ShellLink() { + Target = @"C:\Windows\Notepad.exe", + Description = "It's Notepad", + }; + + sl.SetAppUserModelId("org.anaïsbetts.test"); + var path = Path.GetFullPath(@".\test.lnk"); + sl.Save(path); + + Console.WriteLine("Saved to " + path); + } + + [Fact] + public void RemoveByteOrderMarkerIfPresent() + { + var utf32Be = new byte[] { 0x00, 0x00, 0xFE, 0xFF }; + var utf32Le = new byte[] { 0xFF, 0xFE, 0x00, 0x00 }; + var utf16Be = new byte[] { 0xFE, 0xFF }; + var utf16Le = new byte[] { 0xFF, 0xFE }; + var utf8 = new byte[] { 0xEF, 0xBB, 0xBF }; + + var utf32BeHelloWorld = combine(utf32Be, Encoding.UTF8.GetBytes("hello world")); + var utf32LeHelloWorld = combine(utf32Le, Encoding.UTF8.GetBytes("hello world")); + var utf16BeHelloWorld = combine(utf16Be, Encoding.UTF8.GetBytes("hello world")); + var utf16LeHelloWorld = combine(utf16Le, Encoding.UTF8.GetBytes("hello world")); + var utf8HelloWorld = combine(utf8, Encoding.UTF8.GetBytes("hello world")); + + var asciiMultipleChars = Encoding.ASCII.GetBytes("hello world"); + var asciiSingleChar = Encoding.ASCII.GetBytes("A"); + + var emptyString = string.Empty; + string nullString = null; + byte[] nullByteArray = { }; + Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(emptyString)); + Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(nullString)); + Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(nullByteArray)); + + Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(utf32Be)); + Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(utf32Le)); + Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(utf16Be)); + Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(utf16Le)); + Assert.Equal(string.Empty, Utility.RemoveByteOrderMarkerIfPresent(utf8)); + + Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(utf32BeHelloWorld)); + Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(utf32LeHelloWorld)); + Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(utf16BeHelloWorld)); + Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(utf16LeHelloWorld)); + Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(utf8HelloWorld)); + + Assert.Equal("hello world", Utility.RemoveByteOrderMarkerIfPresent(asciiMultipleChars)); + Assert.Equal("A", Utility.RemoveByteOrderMarkerIfPresent(asciiSingleChar)); + } + + [Fact] + public void ShaCheckShouldBeCaseInsensitive() + { + var sha1FromExternalTool = "75255cfd229a1ed1447abe1104f5635e69975d30"; + var inputPackage = PathHelper.GetFixture("Squirrel.Core.1.0.0.0.nupkg"); + var stream = File.OpenRead(inputPackage); + var sha1 = Utility.CalculateStreamSHA1(stream); + + Assert.NotEqual(sha1FromExternalTool, sha1); + Assert.Equal(sha1FromExternalTool, sha1, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public void CanDeleteDeepRecursiveDirectoryStructure() + { + using var logger = _output.BuildLoggerFor(); + string tempDir; + using (Utility.GetTempDirectory(out tempDir)) { + for (var i = 0; i < 50; i++) { + var directory = Path.Combine(tempDir, newId()); + CreateSampleDirectory(directory); + } + + var files = Directory.GetFiles(tempDir, "*", SearchOption.AllDirectories); + + var count = files.Count(); + + logger.Info($"Created {count} files under directory {tempDir}"); + + var sw = new Stopwatch(); + sw.Start(); + Utility.DeleteFileOrDirectoryHard(tempDir); + sw.Stop(); + logger.Info($"Delete took {sw.ElapsedMilliseconds}ms"); + + Assert.False(Directory.Exists(tempDir)); + } + } + + //[Fact] + //public void CreateFakePackageSmokeTest() + //{ + // string path; + // using (Utility.GetTempDirectory(out path)) { + // var output = IntegrationTestHelper.CreateFakeInstalledApp("0.3.0", path); + // Assert.True(File.Exists(output)); + // } + //} + + [Theory] + [InlineData("foo.dll", true)] + [InlineData("foo.DlL", true)] + [InlineData("C:\\Foo\\Bar\\foo.Exe", true)] + [InlineData("Test.png", false)] + [InlineData(".rels", false)] + public void FileIsLikelyPEImageTest(string input, bool result) + { + Assert.Equal(result, Utility.FileIsLikelyPEImage(input)); + } + + [Fact(Skip = "Only really need to run this test after changes to FileDownloader")] + public async Task DownloaderReportsProgress() + { + // this probably should use a local http server instead. + const string testUrl = "http://speedtest.tele2.net/1MB.zip"; + + var dl = Utility.CreateDefaultDownloader(); + + List prog = new List(); + using (Utility.GetTempFileName(out var tempPath)) + await dl.DownloadFile(testUrl, tempPath, prog.Add); + + Assert.True(prog.Count > 10); + Assert.Equal(100, prog.Last()); + Assert.True(prog[1] != 0); + } + + static void CreateSampleDirectory(string directory) + { + Random prng = new Random(); + while (true) { + Directory.CreateDirectory(directory); + + for (var j = 0; j < 100; j++) { + var file = Path.Combine(directory, newId()); + if (file.Length > 260) continue; + File.WriteAllText(file, Guid.NewGuid().ToString()); + } + + if (prng.NextDouble() > 0.5) { + var childDirectory = Path.Combine(directory, newId()); + if (childDirectory.Length > 248) return; + directory = childDirectory; + continue; + } + + break; + } + } + + static string newId() + { + var text = Guid.NewGuid().ToString(); + var bytes = Encoding.Unicode.GetBytes(text); + var provider = SHA1.Create(); + var hashString = string.Empty; + + foreach (var x in provider.ComputeHash(bytes)) { + hashString += String.Format("{0:x2}", x); + } + + if (hashString.Length > 7) { + return hashString.Substring(0, 7); + } + + return hashString; + } + + static byte[] combine(params byte[][] arrays) + { + var rv = new byte[arrays.Sum(a => a.Length)]; + var offset = 0; + foreach (var array in arrays) { + Buffer.BlockCopy(array, 0, rv, offset, array.Length); + offset += array.Length; + } + return rv; + } + } diff --git a/test/Velopack.Tests/ZipPackageTests.cs b/test/Velopack.Tests/ZipPackageTests.cs index 367b13cf..4a4dd4da 100644 --- a/test/Velopack.Tests/ZipPackageTests.cs +++ b/test/Velopack.Tests/ZipPackageTests.cs @@ -4,85 +4,84 @@ using Velopack.NuGet; using Velopack.Tests.TestHelpers; using ZipPackage = Velopack.NuGet.ZipPackage; -namespace Velopack.Tests +namespace Velopack.Tests; + +public class ZipPackageTests { - public class ZipPackageTests + [Fact] + public void HasSameFilesAndDependenciesAsPackaging() { - [Fact] - public void HasSameFilesAndDependenciesAsPackaging() - { - using var _1 = Utility.GetTempDirectory(out var tempDir); - var inputPackage = PathHelper.GetFixture("slack-1.1.8-full.nupkg"); - var copyPackage = Path.Combine(tempDir, "slack-1.1.8-full.nupkg"); - File.Copy(inputPackage, copyPackage); + using var _1 = Utility.GetTempDirectory(out var tempDir); + var inputPackage = PathHelper.GetFixture("slack-1.1.8-full.nupkg"); + var copyPackage = Path.Combine(tempDir, "slack-1.1.8-full.nupkg"); + File.Copy(inputPackage, copyPackage); - var zp = new ZipPackage(inputPackage); - var zipf = zp.Files.OrderBy(f => f.Path).ToArray(); - var zipfLib = zp.Files.Where(f => f.IsLibFile()).OrderBy(f => f.Path).ToArray(); + var zp = new ZipPackage(inputPackage); + var zipf = zp.Files.OrderBy(f => f.Path).ToArray(); + var zipfLib = zp.Files.Where(f => f.IsLibFile()).OrderBy(f => f.Path).ToArray(); - using Package package = Package.Open(copyPackage); - var packaging = GetFiles(package).OrderBy(f => f.Path).ToArray(); - var packagingLib = GetLibFiles(package).OrderBy(f => f.Path).ToArray(); + using Package package = Package.Open(copyPackage); + var packaging = GetFiles(package).OrderBy(f => f.Path).ToArray(); + var packagingLib = GetLibFiles(package).OrderBy(f => f.Path).ToArray(); - //for (int i = 0; i < zipf.Length; i++) { - // if (zipf[i] != packagingLib[i]) - // throw new Exception(); - //} + //for (int i = 0; i < zipf.Length; i++) { + // if (zipf[i] != packagingLib[i]) + // throw new Exception(); + //} - Assert.Equal(packaging, zipf); - Assert.Equal(packagingLib, zipfLib); - } + Assert.Equal(packaging, zipf); + Assert.Equal(packagingLib, zipfLib); + } - [Fact] - public void ParsesNuspecCorrectly() - { - var inputPackage = PathHelper.GetFixture("FullNuspec.1.0.0.nupkg"); - var zp = new ZipPackage(inputPackage); + [Fact] + public void ParsesNuspecCorrectly() + { + var inputPackage = PathHelper.GetFixture("FullNuspec.1.0.0.nupkg"); + var zp = new ZipPackage(inputPackage); - var dyn = ExposedObject.From(zp); + var dyn = ExposedObject.From(zp); - Assert.Equal("FullNuspec", zp.Id); - Assert.Equal(SemanticVersion.Parse("1.0.0"), zp.Version); - Assert.Equal(new[] { "Anaïs Betts", "Caelan Sayler" }, dyn.Authors); - Assert.Equal(new Uri("https://github.com/clowd/Clowd.Squirrel"), zp.ProjectUrl); - Assert.Equal(new Uri("https://user-images.githubusercontent.com/1287295/131249078-9e131e51-0b66-4dc7-8c0a-99cbea6bcf80.png"), zp.IconUrl); - Assert.Equal("A test description", dyn.Description); - Assert.Equal("A summary", dyn.Summary); - Assert.Equal("release notes\nwith multiple lines", zp.ReleaseNotes); - Assert.Equal("Copyright ©", dyn.Copyright); - Assert.Equal("en-US", zp.Language); - Assert.Equal("Squirrel for Windows", dyn.Title); - } + Assert.Equal("FullNuspec", zp.Id); + Assert.Equal(SemanticVersion.Parse("1.0.0"), zp.Version); + Assert.Equal(new[] { "Anaïs Betts", "Caelan Sayler" }, dyn.Authors); + Assert.Equal(new Uri("https://github.com/clowd/Clowd.Squirrel"), zp.ProjectUrl); + Assert.Equal(new Uri("https://user-images.githubusercontent.com/1287295/131249078-9e131e51-0b66-4dc7-8c0a-99cbea6bcf80.png"), zp.IconUrl); + Assert.Equal("A test description", dyn.Description); + Assert.Equal("A summary", dyn.Summary); + Assert.Equal("release notes\nwith multiple lines", zp.ReleaseNotes); + Assert.Equal("Copyright ©", dyn.Copyright); + Assert.Equal("en-US", zp.Language); + Assert.Equal("Squirrel for Windows", dyn.Title); + } - IEnumerable GetLibFiles(Package package) - { - return GetFiles(package, NugetUtil.LibDirectory); - } + IEnumerable GetLibFiles(Package package) + { + return GetFiles(package, NugetUtil.LibDirectory); + } - IEnumerable GetFiles(Package package, string directory) - { - string folderPrefix = directory + Path.DirectorySeparatorChar; - return GetFiles(package).Where(file => file.Path.StartsWith(folderPrefix, StringComparison.OrdinalIgnoreCase)); - } + IEnumerable GetFiles(Package package, string directory) + { + string folderPrefix = directory + Path.DirectorySeparatorChar; + return GetFiles(package).Where(file => file.Path.StartsWith(folderPrefix, StringComparison.OrdinalIgnoreCase)); + } - List GetFiles(Package package) - { - return (from part in package.GetParts() - where IsPackageFile(part) - select new ZipPackageFile(part.Uri)).ToList(); - } + List GetFiles(Package package) + { + return (from part in package.GetParts() + where IsPackageFile(part) + select new ZipPackageFile(part.Uri)).ToList(); + } - bool IsPackageFile(PackagePart part) - { - string path = NugetUtil.GetPath(part.Uri); - string directory = Path.GetDirectoryName(path); - string[] ExcludePaths = new[] { "_rels", "package" }; - return !ExcludePaths.Any(p => directory.StartsWith(p, StringComparison.OrdinalIgnoreCase)) && !IsManifest(path); - } + bool IsPackageFile(PackagePart part) + { + string path = NugetUtil.GetPath(part.Uri); + string directory = Path.GetDirectoryName(path); + string[] ExcludePaths = new[] { "_rels", "package" }; + return !ExcludePaths.Any(p => directory.StartsWith(p, StringComparison.OrdinalIgnoreCase)) && !IsManifest(path); + } - bool IsManifest(string p) - { - return Path.GetExtension(p).Equals(NugetUtil.ManifestExtension, StringComparison.OrdinalIgnoreCase); - } + bool IsManifest(string p) + { + return Path.GetExtension(p).Equals(NugetUtil.ManifestExtension, StringComparison.OrdinalIgnoreCase); } }