Convert all files to file-scoped name space

This commit is contained in:
Caelan Sayler
2024-02-02 12:26:31 +00:00
parent c10b15278f
commit 2caa7852d3
59 changed files with 5196 additions and 5256 deletions

View File

@@ -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<string, string>() {
{ "ARCH", arch }
};
logger.Info("About to create .AppImage for architecture: " + arch);
var envVar = new Dictionary<string, string>() {
{ "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);
}
}

View File

@@ -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<LinuxPackOptions>
{
[SupportedOSPlatform("linux")]
public class LinuxPackCommandRunner : PackageBuilder<LinuxPackOptions>
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<string> PreprocessPackDir(Action<int> progress, string packDir)
{
var dir = TempDir.CreateSubdirectory("PreprocessPackDir.AppDir");
var bin = dir.CreateSubdirectory("usr").CreateSubdirectory("bin");
protected override Task<string> PreprocessPackDir(Action<int> 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<int> 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<int> 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<int> 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<string> CreateDeltaPackage(Action<int> 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<int> 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<string> CreateDeltaPackage(Action<int> 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);
}
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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<int> progress)
{
Queue<string> pendingSign = new Queue<string>();
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<string> filesToSign = new List<string>();
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<ProcessResult> 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<int> progress)
{
Queue<string> pendingSign = new Queue<string>();
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<string> filesToSign = new List<string>();
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<ProcessResult> 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());
//}
}

View File

@@ -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>(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>(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<BundleFile> 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<BundleFile> 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;
}
}
}

View File

@@ -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<string> args = new List<string>() {
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<string> args = new List<string>() {
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));
}
}

View File

@@ -1,9 +1,8 @@
using Microsoft.Extensions.Logging;
namespace Velopack.Packaging.Abstractions
namespace Velopack.Packaging.Abstractions;
public interface ICommand<TOpt> where TOpt : class
{
public interface ICommand<TOpt> where TOpt : class
{
Task Run(TOpt options);
}
Task Run(TOpt options);
}

View File

@@ -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<IFancyConsoleProgress, Task> action);
Task ExecuteProgressAsync(Func<IFancyConsoleProgress, Task> action);
void WriteTable(string tableName, IEnumerable<IEnumerable<string>> rows, bool hasHeaderRow = true);
void WriteTable(string tableName, IEnumerable<IEnumerable<string>> rows, bool hasHeaderRow = true);
Task<bool> PromptYesNo(string prompt, bool? defaultValue = null, TimeSpan? timeout = null);
Task<bool> PromptYesNo(string prompt, bool? defaultValue = null, TimeSpan? timeout = null);
void WriteLine(string text = "");
}
void WriteLine(string text = "");
}

View File

@@ -1,7 +1,6 @@
namespace Velopack.Packaging.Abstractions
namespace Velopack.Packaging.Abstractions;
public interface IFancyConsoleProgress
{
public interface IFancyConsoleProgress
{
Task RunTask(string name, Func<Action<int>, Task> fn);
}
Task RunTask(string name, Func<Action<int>, Task> fn);
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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<string> Files { get; set; } = new List<string>();
public List<VelopackAsset> GetReleaseEntries()
{
public List<string> Files { get; set; } = new List<string>();
return Files.Where(x => x.EndsWith(".nupkg"))
.Select(f => VelopackAsset.FromNupkg(f))
.ToList();
}
public List<VelopackAsset> GetReleaseEntries()
{
return Files.Where(x => x.EndsWith(".nupkg"))
.Select(f => VelopackAsset.FromNupkg(f))
.ToList();
}
public static void Write(string outputDir, string channel, IEnumerable<string> 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<string> 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<BuildAssets>(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<BuildAssets>(File.ReadAllText(path));
}
}

View File

@@ -1,31 +1,30 @@
using Microsoft.Extensions.Logging;
using Velopack.Packaging.Abstractions;
namespace Velopack.Packaging.Commands
namespace Velopack.Packaging.Commands;
public class DeltaGenCommandRunner : ICommand<DeltaGenOptions>
{
public class DeltaGenCommandRunner : ICommand<DeltaGenOptions>
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;
});
}
});
}
}

View File

@@ -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; }
}

View File

@@ -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<DeltaPatchOptions>
{
public class DeltaPatchCommandRunner : ICommand<DeltaPatchOptions>
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);
});
}
});
}
}

View File

@@ -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; }
}

View File

@@ -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<int> 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<int> 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);
}
}
}

View File

@@ -6,28 +6,27 @@ using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
namespace Velopack.Packaging.Exceptions
namespace Velopack.Packaging.Exceptions;
/// <summary>
/// Denotes that an error has occurred for which a stack trace should not be printed.
/// </summary>
[ExcludeFromCodeCoverage]
public class UserInfoException : Exception
{
/// <summary>
/// Denotes that an error has occurred for which a stack trace should not be printed.
/// </summary>
[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)
{
}
}

View File

@@ -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.")
{
}
}
}

View File

@@ -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<string> args, string workingDir, IDictionary<string, string> 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<string> args, string workingDirectory, CancellationToken ct = default, IDictionary<string, string> 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<string> args, string workingDir, IDictionary<string, string> 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<string> args, string workingDirectory, CancellationToken ct = default, IDictionary<string, string> 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;
}
}

View File

@@ -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<T> : ICommand<T>
where T : class, IPackOptions
{
public abstract class PackageBuilder<T> : ICommand<T>
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 = $"""
<releaseNotes>{SecurityElement.Escape(markdown)}</releaseNotes>
<releaseNotesHtml><![CDATA[{"\n"}{html}{"\n"}]]></releaseNotesHtml>
""";
}
}
string osMinVersionText = "";
if (rid?.HasVersion == true) {
osMinVersionText = $"<osMinVersion>{rid.Version}</osMinVersion>";
}
string osMinVersionText = "";
if (rid?.HasVersion == true) {
osMinVersionText = $"<osMinVersion>{rid.Version}</osMinVersion>";
}
string machineArchitectureText = "";
if (rid?.HasArchitecture == true) {
machineArchitectureText = $"<machineArchitecture>{rid.Architecture}</machineArchitecture>";
}
string machineArchitectureText = "";
if (rid?.HasArchitecture == true) {
machineArchitectureText = $"<machineArchitecture>{rid.Architecture}</machineArchitecture>";
}
string runtimeDependenciesText = "";
if (!String.IsNullOrWhiteSpace(RuntimeDependencies)) {
runtimeDependenciesText = $"<runtimeDependencies>{RuntimeDependencies}</runtimeDependencies>";
}
string runtimeDependenciesText = "";
if (!String.IsNullOrWhiteSpace(RuntimeDependencies)) {
runtimeDependenciesText = $"<runtimeDependencies>{RuntimeDependencies}</runtimeDependencies>";
}
string nuspec = $"""
string nuspec = $"""
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
@@ -227,93 +227,93 @@ namespace Velopack.Packaging
</package>
""".Trim();
return nuspec;
return nuspec;
}
protected abstract Task<string> PreprocessPackDir(Action<int> progress, string packDir);
protected virtual Task CodeSign(Action<int> progress, string packDir)
{
return Task.CompletedTask;
}
protected abstract Task CreatePortablePackage(Action<int> progress, string packDir, string outputPath);
protected virtual Task<string> CreateDeltaPackage(Action<int> 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<int> progress, string releasePkg, string packDir, string outputPath)
{
return Task.CompletedTask;
}
protected virtual async Task CreateReleasePackage(Action<int> 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<string> PreprocessPackDir(Action<int> progress, string packDir);
AddContentTypesAndRel(nuspecPath);
protected virtual Task CodeSign(Action<int> progress, string packDir)
await EasyZip.CreateZipFromDirectoryAsync(Log, outputPath, stagingDir.FullName, Utility.CreateProgressDelegate(progress, 30, 100));
progress(100);
}
protected virtual Dictionary<string, string> GetReleaseMetadataFiles()
{
return new Dictionary<string, string>();
}
protected virtual void CopyFiles(DirectoryInfo source, DirectoryInfo target, Action<int> 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<int> progress, string packDir, string outputPath);
protected virtual Task<string> CreateDeltaPackage(Action<int> 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<int> progress, string releasePkg, string packDir, string outputPath)
{
return Task.CompletedTask;
}
protected virtual async Task CreateReleasePackage(Action<int> 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<string, string> GetReleaseMetadataFiles()
{
return new Dictionary<string, string>();
}
protected virtual void CopyFiles(DirectoryInfo source, DirectoryInfo target, Action<int> 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 => $""" <Default Extension="{ext}" ContentType="application/octet" />""")
.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 => $""" <Default Extension="{ext}" ContentType="application/octet" />""")
.ToArray();
var contentType = $"""
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
@@ -321,18 +321,17 @@ namespace Velopack.Packaging
</Types>
""";
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 = $"""
<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Type="http://schemas.microsoft.com/packaging/2010/07/manifest" Target="/{Path.GetFileName(nuspecPath)}" Id="R1" />
</Relationships>
""";
File.WriteAllText(Path.Combine(relsDir, ".rels"), rels);
}
File.WriteAllText(Path.Combine(relsDir, ".rels"), rels);
}
}

View File

@@ -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<string, List<VelopackAsset>> _releases;
public ReleaseEntryHelper(string outputDir, string channel, ILogger logger)
{
private readonly string _outputDir;
private readonly ILogger _logger;
private readonly string _channel;
private Dictionary<string, List<VelopackAsset>> _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<string, List<VelopackAsset>> GetReleasesFromDir(string dir)
{
var rel = new Dictionary<string, List<VelopackAsset>>(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<VelopackAsset>();
rel[ch].Add(VelopackAsset.FromZipPackage(zip));
}
return rel;
}
private static Dictionary<string, List<VelopackAsset>> GetReleasesFromDir(string dir)
{
var rel = new Dictionary<string, List<VelopackAsset>>(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<VelopackAsset>();
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<VelopackAsset> GetLatestAssets()
{
if (!_releases.ContainsKey(_channel) || !_releases[_channel].Any())
return Enumerable.Empty<VelopackAsset>();
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<VelopackAsset> GetLatestAssets()
{
if (!_releases.ContainsKey(_channel) || !_releases[_channel].Any())
return Enumerable.Empty<VelopackAsset>();
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<VelopackAsset> MergeAssets(IEnumerable<VelopackAsset> priority, IEnumerable<VelopackAsset> 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<FileInfo> Files { get; } = new List<FileInfo>();
// public List<ReleaseEntry> Releases { get; } = new List<ReleaseEntry>();
//}
//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<VelopackAsset> MergeAssets(IEnumerable<VelopackAsset> priority, IEnumerable<VelopackAsset> 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<FileInfo> Files { get; } = new List<FileInfo>();
// public List<ReleaseEntry> Releases { get; } = new List<ReleaseEntry>();
//}
//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;
//}
}

View File

@@ -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<DeltaMode>((v) => DeltaMode = v, "--mode")
.SetDefault(DeltaMode.BestSpeed)
.SetDescription("Set the delta generation mode.");
public string BasePackage { get; set; }
AddOption<FileInfo>((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<FileInfo>((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<DeltaMode>((v) => DeltaMode = v, "--mode")
.SetDefault(DeltaMode.BestSpeed)
.SetDescription("Set the delta generation mode.");
AddOption<FileInfo>((v) => BasePackage = v.ToFullNameOrNull(), "--base", "-b")
.SetDescription("The base package for the created patch.")
.SetArgumentHelpName("PATH")
.RequiresExtension(".nupkg")
.MustExist()
.SetRequired();
AddOption<FileInfo>((v) => NewPackage = v.ToFullNameOrNull(), "--new", "-n")
.SetDescription("The resulting package for the created patch.")
.SetArgumentHelpName("PATH")
.RequiresExtension(".nupkg")
.MustExist()
.SetRequired();
AddOption<FileInfo>((v) => OutputFile = v.ToFullNameOrNull(), "--output", "-o")
.SetDescription("The output file path for the created patch.")
.SetArgumentHelpName("PATH")
.SetRequired();
}
AddOption<FileInfo>((v) => OutputFile = v.ToFullNameOrNull(), "--output", "-o")
.SetDescription("The output file path for the created patch.")
.SetArgumentHelpName("PATH")
.SetRequired();
}
}

View File

@@ -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<FileInfo>((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<FileInfo[]>((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<FileInfo>((v) => BasePackage = v.ToFullNameOrNull(), "--base", "-b")
.SetDescription("The base package for the created patch.")
.SetArgumentHelpName("PATH")
.RequiresExtension(".nupkg")
.MustExist()
.SetRequired();
AddOption<FileInfo[]>((v) => PatchFiles = v, "--patch", "-p")
.SetDescription("The resulting package for the created patch.")
.AllowMultiple()
.SetArgumentHelpName("PATH");
AddOption<FileInfo>((v) => OutputFile = v.ToFullNameOrNull(), "--output", "-o")
.SetDescription("The output file path for the created patch.")
.SetArgumentHelpName("PATH")
.SetRequired();
}
AddOption<FileInfo>((v) => OutputFile = v.ToFullNameOrNull(), "--output", "-o")
.SetDescription("The output file path for the created patch.")
.SetArgumentHelpName("PATH")
.SetRequired();
}
}

View File

@@ -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<string>((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<string>((v) => PackVersion = v, "--packVersion", "-v")
.SetDescription("Current version for application bundle.")
.SetArgumentHelpName("VERSION")
.SetRequired()
.RequiresSemverCompliant();
public string PackDirectory { get; private set; }
var packDir = AddOption<DirectoryInfo>((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p")
.SetDescription("Directory containing application files from dotnet publish")
.SetArgumentHelpName("DIR")
.MustNotBeEmpty();
public string PackAuthors { get; private set; }
AddOption<string>((v) => PackAuthors = v, "--packAuthors")
.SetDescription("Company name or comma-delimited list of authors.")
.SetArgumentHelpName("AUTHORS");
public string PackTitle { get; private set; }
AddOption<string>((v) => PackTitle = v, "--packTitle")
.SetDescription("Display/friendly name for application.")
.SetArgumentHelpName("NAME");
public string EntryExecutableName { get; private set; }
AddOption<string>((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<FileInfo>((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon")
.SetDescription("Path to the icon file for this bundle.")
.SetArgumentHelpName("PATH")
.MustExist();
public string ReleaseNotes { get; set; }
AddOption<FileInfo>((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes")
.SetDescription("File with markdown-formatted notes for this version.")
.SetArgumentHelpName("PATH")
.MustExist();
public bool PackIsAppDir { get; private set; }
AddOption<DeltaMode>((v) => DeltaMode = v, "--delta")
.SetDefault(DeltaMode.BestSpeed)
.SetDescription("Set the delta generation mode.");
public DeltaMode DeltaMode { get; set; } = DeltaMode.BestSpeed;
var appDir = AddOption<DirectoryInfo>((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<string>((v) => PackId = v, "--packId", "-u")
.SetDescription("Unique Id for application bundle.")
.SetArgumentHelpName("ID")
.SetRequired()
.RequiresValidNuGetId();
// TODO add parser straight to SemanticVersion?
AddOption<string>((v) => PackVersion = v, "--packVersion", "-v")
.SetDescription("Current version for application bundle.")
.SetArgumentHelpName("VERSION")
.SetRequired()
.RequiresSemverCompliant();
var packDir = AddOption<DirectoryInfo>((v) => PackDirectory = v.ToFullNameOrNull(), "--packDir", "-p")
.SetDescription("Directory containing application files from dotnet publish")
.SetArgumentHelpName("DIR")
.MustNotBeEmpty();
AddOption<string>((v) => PackAuthors = v, "--packAuthors")
.SetDescription("Company name or comma-delimited list of authors.")
.SetArgumentHelpName("AUTHORS");
AddOption<string>((v) => PackTitle = v, "--packTitle")
.SetDescription("Display/friendly name for application.")
.SetArgumentHelpName("NAME");
AddOption<string>((v) => EntryExecutableName = v, "-e", "--mainExe")
.SetDescription("The file name of the main/entry executable.")
.SetArgumentHelpName("NAME");
var icon = AddOption<FileInfo>((v) => Icon = v.ToFullNameOrNull(), "-i", "--icon")
.SetDescription("Path to the icon file for this bundle.")
.SetArgumentHelpName("PATH")
.MustExist();
AddOption<FileInfo>((v) => ReleaseNotes = v.ToFullNameOrNull(), "--releaseNotes")
.SetDescription("File with markdown-formatted notes for this version.")
.SetArgumentHelpName("PATH")
.MustExist();
AddOption<DeltaMode>((v) => DeltaMode = v, "--delta")
.SetDefault(DeltaMode.BestSpeed)
.SetDescription("Set the delta generation mode.");
var appDir = AddOption<DirectoryInfo>((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);
}
}

View File

@@ -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<bool>
{
public class LongHelpCommand : CliOption<bool>
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<IRenderable> 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<IRenderable> 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<CliOption> 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<CliOption>();
//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<CliOption> 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<string> GetUsageParts()
{
bool displayOptionTitle = false;
IEnumerable<CliCommand> parentCommands =
command
.RecurseWhileNotNull(c => c.Parents.OfType<CliCommand>().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 "[[--] <additional arguments>...]]";
}
}
}
private string FormatArgumentUsage(IList<CliArgument> arguments)
{
var sb = new StringBuilder(arguments.Count * 100);
var end = default(List<char>);
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<CliOption>();
//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<string> GetUsageParts()
{
bool displayOptionTitle = false;
IEnumerable<CliCommand> parentCommands =
command
.RecurseWhileNotNull(c => c.Parents.OfType<CliCommand>().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 "[[--] <additional arguments>...]]";
}
}
}
}
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<T> RecurseWhileNotNull<T>(
this T source,
Func<T, T> next)
where T : class
private string FormatArgumentUsage(IList<CliArgument> arguments)
{
while (source is not null) {
yield return source;
var sb = new StringBuilder(arguments.Count * 100);
source = next(source);
var end = default(List<char>);
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<T> RecurseWhileNotNull<T>(
this T source,
Func<T, T> next)
where T : class
{
while (source is not null) {
yield return source;
source = next(source);
}
}
}

View File

@@ -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<DirectoryInfo> ReleaseDirectoryOption { get; private set; }
protected CliOption<string> ChannelOption { get; private set; }
protected OutputCommand(string name, string description)
: base(name, description)
{
public string ReleaseDir { get; private set; }
ReleaseDirectoryOption = AddOption<DirectoryInfo>((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<string>((v) => Channel = v, "-c", "--channel")
.SetDescription("The channel to use for this release.")
.RequiresValidNuGetId()
.SetArgumentHelpName("NAME")
.SetDefault(ReleaseEntryHelper.GetDefaultChannel(VelopackRuntimeInfo.SystemOs));
}
protected CliOption<DirectoryInfo> ReleaseDirectoryOption { get; private set; }
protected CliOption<string> ChannelOption { get; private set; }
protected OutputCommand(string name, string description)
: base(name, description)
{
ReleaseDirectoryOption = AddOption<DirectoryInfo>((v) => ReleaseDir = v.ToFullNameOrNull(), "-o", "--outputDir")
.SetDescription("Output directory for created packages.")
.SetArgumentHelpName("DIR")
.SetDefault(new DirectoryInfo("Releases"));
ChannelOption = AddOption<string>((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;
}
}

View File

@@ -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<string> TargetRuntimeOption { get; private set; }
protected PlatformCommand(string name, string description) : base(name, description)
{
public string TargetRuntime { get; set; }
protected CliOption<string> TargetRuntimeOption { get; private set; }
protected PlatformCommand(string name, string description) : base(name, description)
{
TargetRuntimeOption = AddOption<string>((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<string>((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;
}

View File

@@ -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<IFancyConsoleProgress, Task> action)
{
var start = DateTime.UtcNow;
await action(new Progress(logger));
logger.Info($"Finished in {DateTime.UtcNow - start}.");
}
public Task<bool> 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<IEnumerable<string>> 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<IFancyConsoleProgress, Task> action)
public async Task RunTask(string name, Func<Action<int>, Task> fn)
{
var start = DateTime.UtcNow;
await action(new Progress(logger));
logger.Info($"Finished in {DateTime.UtcNow - start}.");
}
public Task<bool> 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<IEnumerable<string>> 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<Action<int>, 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);
}
}
}

View File

@@ -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)
{
}
}

View File

@@ -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<IFancyConsoleProgress, Task> 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<bool> 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<char>(textPrompt, comparer);
clip.Choices.Add('y');
clip.Choices.Add('n');
clip.ShowChoices = true;
clip.ShowDefaultValue = true;
clip.DefaultValue = new DefaultPromptValue<char>(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<IFancyConsoleProgress, Task> 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<bool> 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<IEnumerable<string>> 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<char>(textPrompt, comparer);
clip.Choices.Add('y');
clip.Choices.Add('n');
clip.ShowChoices = true;
clip.ShowDefaultValue = true;
clip.DefaultValue = new DefaultPromptValue<char>(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<IEnumerable<string>> 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<Action<int>, 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<Action<int>, 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}[/]");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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>(TState state) where TState : notnull
{
public IDisposable BeginScope<TState>(TState state) where TState : notnull
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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

View File

@@ -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<DotnetUtilTests>();
Assert.Null(DotnetUtil.VerifyVelopackApp(PathHelper.GetRustAsset("testapp.exe"), logger));
}
[SkippableFact]
public void NonDotnetBinaryPasses()
{
Skip.IfNot(VelopackRuntimeInfo.IsWindows);
using var logger = _output.BuildLoggerFor<DotnetUtilTests>();
Assert.Null(DotnetUtil.VerifyVelopackApp(PathHelper.GetRustAsset("testapp.exe"), logger));
}
[SkippableFact]
public void PublishSingleFilePasses()
{
Skip.IfNot(VelopackRuntimeInfo.IsWindows);
using var logger = _output.BuildLoggerFor<DotnetUtilTests>();
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<DotnetUtilTests>();
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<DotnetUtilTests>();
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<DotnetUtilTests>();
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<DotnetUtilTests>();
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<DotnetUtilTests>();
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<DotnetUtilTests>();
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<DotnetUtilTests>();
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<UserInfoException>(() => DotnetUtil.VerifyVelopackApp(path, logger));
}
var path = Path.Combine(dir, "TestApp.exe");
Assert.Throws<UserInfoException>(() => DotnetUtil.VerifyVelopackApp(path, logger));
}
}

View File

@@ -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<GithubDeploymentTests>();
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<GithubDeploymentTests>();
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<UserInfoException>(() => 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<GithubDeploymentTests>();
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<UserInfoException>(() => 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<GithubDeploymentTests>();
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<UserInfoException>(() => 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<GithubDeploymentTests>();
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<UserInfoException>(() => 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<GithubDeploymentTests>();
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<GithubDeploymentTests>();
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<GithubDeploymentTests>();
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}'");
}
}
}
}

View File

@@ -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());
}
}

View File

@@ -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<S3DeploymentTests>();
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<S3DeploymentTests>();
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.");
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<ReleaseEntry> 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<ReleaseEntry> 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<ReleaseEntry> 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<ReleaseEntry> 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<ReleaseEntry> BuildReleasesFile(string releasePackagesDir)
//{
// var packagesDir = new DirectoryInfo(releasePackagesDir);
// // Generate release entries for all of the local packages
// var entriesQueue = new ConcurrentQueue<ReleaseEntry>();
// 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<ReleaseEntry> 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<ReleaseEntry> 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<ReleaseEntry> 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<ReleaseEntry> 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<ReleaseEntry> 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<ReleaseEntry> BuildReleasesFile(string releasePackagesDir)
//{
// var packagesDir = new DirectoryInfo(releasePackagesDir);
// // Generate release entries for all of the local packages
// var entriesQueue = new ConcurrentQueue<ReleaseEntry>();
// 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<ReleaseEntry> 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();
//}
}

View File

@@ -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);
}
}

View File

@@ -1,302 +1,301 @@
using System.Globalization;
using System.Text.RegularExpressions;
namespace Velopack.Tests.OldSquirrel
namespace Velopack.Tests.OldSquirrel;
/// <summary>
/// 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.
/// </summary>
[Serializable]
public sealed class SemanticVersion : IComparable, IComparable<SemanticVersion>, IEquatable<SemanticVersion>
{
/// <summary>
/// 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.
/// </summary>
[Serializable]
public sealed class SemanticVersion : IComparable, IComparable<SemanticVersion>, IEquatable<SemanticVersion>
private const RegexOptions _flags = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture;
private static readonly Regex _semanticVersionRegex = new Regex(@"^(?<Version>\d+(\s*\.\s*\d+){0,3})(?<Release>-[a-z][0-9a-z-]*)?$", _flags);
private static readonly Regex _strictSemanticVersionRegex = new Regex(@"^(?<Version>\d+(\.\d+){2})(?<Release>-[a-z][0-9a-z-]*)?$", _flags);
private static readonly Regex _preReleaseVersionRegex = new Regex(@"(?<PreReleaseString>[a-z]+)(?<PreReleaseNumber>[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(@"^(?<Version>\d+(\s*\.\s*\d+){0,3})(?<Release>-[a-z][0-9a-z-]*)?$", _flags);
private static readonly Regex _strictSemanticVersionRegex = new Regex(@"^(?<Version>\d+(\.\d+){2})(?<Release>-[a-z][0-9a-z-]*)?$", _flags);
private static readonly Regex _preReleaseVersionRegex = new Regex(@"(?<PreReleaseString>[a-z]+)(?<PreReleaseNumber>[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)
{
}
/// <summary>
/// Gets the normalized version portion.
/// </summary>
public Version Version {
get;
private set;
}
public SemanticVersion(Version version)
: this(version, String.Empty)
{
}
/// <summary>
/// Gets the optional special version.
/// </summary>
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;
}
/// <summary>
/// Gets the normalized version portion.
/// </summary>
public Version Version {
get;
private set;
}
/// <summary>
/// Gets the optional special version.
/// </summary>
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;
}
}
/// <summary>
/// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version.
/// </summary>
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;
}
/// <summary>
/// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version.
/// </summary>
public static bool TryParse(string version, out SemanticVersion value)
{
return TryParseInternal(version, _semanticVersionRegex, out value);
}
/// <summary>
/// Parses a version string using strict semantic versioning rules that allows exactly 3 components and an optional special version.
/// </summary>
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;
}
/// <summary>
/// Attempts to parse the version token as a SemanticVersion.
/// </summary>
/// <returns>An instance of SemanticVersion if it parses correctly, null otherwise.</returns>
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;
}
}
/// <summary>
/// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version.
/// </summary>
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;
}
/// <summary>
/// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version.
/// </summary>
public static bool TryParse(string version, out SemanticVersion value)
{
return TryParseInternal(version, _semanticVersionRegex, out value);
}
/// <summary>
/// Parses a version string using strict semantic versioning rules that allows exactly 3 components and an optional special version.
/// </summary>
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;
}
/// <summary>
/// Attempts to parse the version token as a SemanticVersion.
/// </summary>
/// <returns>An instance of SemanticVersion if it parses correctly, null otherwise.</returns>
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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<Exception>(() => 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<Exception>(() => 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<ReleaseEntry>(), 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<ReleaseEntry>(), 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);
}
}
}

View File

@@ -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<string> { "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<string> { "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
}
}

View File

@@ -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<Exception>(() => 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<Exception>(() => 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();
}
}

View File

@@ -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<ShortcutTests>();
string exeName = "NotSquirrelAwareApp.exe";
[SkippableFact]
public void CanCreateAndRemoveShortcuts()
{
Skip.IfNot(VelopackRuntimeInfo.IsWindows);
using var logger = _output.BuildLoggerFor<ShortcutTests>();
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);
}
}

View File

@@ -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<SemanticVersion>
{
public static readonly JsonSerializerOptions Options = new JsonSerializerOptions {
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNameCaseInsensitive = true,
Converters = { new JsonStringEnumConverter(), new SemanticVersionConverter() },
};
internal class SemanticVersionConverter : JsonConverter<SemanticVersion>
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<GithubReleaseAsset>(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<string> { "hi", "there" },
};
var json = JsonSerializer.Serialize(obj, Options);
Assert.Contains("\"Delta\"", json);
var dez = SimpleJson.DeserializeObject<TestClass2>(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<VelopackAssetFeed>(json);
Assert.Equal(21, feed.Assets.Length);
Assert.True(feed.Assets.First().Version == new SemanticVersion(1, 0, 11));
}
public class TestGithubReleaseAsset
{
/// <summary>
/// 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".
/// </summary>
[JsonPropertyName("url")]
public string UrlSomething { get; set; }
/// <summary>
/// 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 <see cref="Url"/> property must
/// be used with an appropriate access token.
/// </summary>
[JsonPropertyName("browser_download_url")]
public string BrowserDownloadUrl { get; set; }
/// <summary> The mime type of this release asset (as detected by GitHub). </summary>
[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<string> 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<string> 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<GithubReleaseAsset>(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<string> { "hi", "there" },
};
var json = JsonSerializer.Serialize(obj, Options);
Assert.Contains("\"Delta\"", json);
var dez = SimpleJson.DeserializeObject<TestClass2>(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<VelopackAssetFeed>(json);
Assert.Equal(21, feed.Assets.Length);
Assert.True(feed.Assets.First().Version == new SemanticVersion(1, 0, 11));
}
public class TestGithubReleaseAsset
{
/// <summary>
/// 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".
/// </summary>
[JsonPropertyName("url")]
public string UrlSomething { get; set; }
/// <summary>
/// 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 <see cref="Url"/> property must
/// be used with an appropriate access token.
/// </summary>
[JsonPropertyName("browser_download_url")]
public string BrowserDownloadUrl { get; set; }
/// <summary> The mime type of this release asset (as detected by GitHub). </summary>
[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<string> 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<string> greetings { get; set; }
}
}

View File

@@ -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<T>(this T compareFrom, T compareTo)
{
Assert.Equal(compareTo, compareFrom);
}
public static void ShouldBeSameAs<T>(this T actual, T expected)
{
Assert.Same(expected, actual);
}
public static void ShouldNotBeSameAs<T>(this T actual, T expected)
{
Assert.NotSame(expected, actual);
}
public static void ShouldBeAssignableFrom<T>(this object instance) where T : class
{
Assert.IsAssignableFrom<T>(instance);
}
public static void ShouldBeType(this object instance, Type type)
{
Assert.IsType(type, instance);
}
public static void ShouldBeType<T>(this object instance)
{
Assert.IsType<T>(instance);
}
public static void ShouldNotBeType<T>(this object instance)
{
Assert.IsNotType<T>(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<T>(this T current, T other) where T : IComparable
{
Assert.True(current.CompareTo(other) > 0, current + " is not greater than " + other);
}
public static void ShouldBeLessThan<T>(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<T>(this T compareFrom, T compareTo)
{
Assert.Equal(compareTo, compareFrom);
}
public static void ShouldBeSameAs<T>(this T actual, T expected)
{
Assert.Same(expected, actual);
}
public static void ShouldNotBeSameAs<T>(this T actual, T expected)
{
Assert.NotSame(expected, actual);
}
public static void ShouldBeAssignableFrom<T>(this object instance) where T : class
{
Assert.IsAssignableFrom<T>(instance);
}
public static void ShouldBeType(this object instance, Type type)
{
Assert.IsType(type, instance);
}
public static void ShouldBeType<T>(this object instance)
{
Assert.IsType<T>(instance);
}
public static void ShouldNotBeType<T>(this object instance)
{
Assert.IsNotType<T>(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<T>(this T current, T other) where T : IComparable
{
Assert.True(current.CompareTo(other) > 0, current + " is not greater than " + other);
}
public static void ShouldBeLessThan<T>(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
}

View File

@@ -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<string, Dictionary<int, List<MethodInfo>>> m_staticMethods;
private Dictionary<string, Dictionary<int, List<MethodInfo>>> m_genStaticMethods;
private ExposedClass(Type type)
{
private Type m_type;
private Dictionary<string, Dictionary<int, List<MethodInfo>>> m_staticMethods;
private Dictionary<string, Dictionary<int, List<MethodInfo>>> 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<MethodInfo> methods = new List<MethodInfo>();
//
// 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<MethodInfo> methods = new List<MethodInfo>();
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);
}
}

View File

@@ -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<string, Dictionary<int, List<MethodInfo>>> m_instanceMethods;
private Dictionary<string, Dictionary<int, List<MethodInfo>>> m_genInstanceMethods;
private ExposedObject(object obj)
{
private object m_object;
private Type m_type;
private Dictionary<string, Dictionary<int, List<MethodInfo>>> m_instanceMethods;
private Dictionary<string, Dictionary<int, List<MethodInfo>>> 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<T>()
{
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<T>(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<MethodInfo> methods = new List<MethodInfo>();
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<T>()
{
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<T>(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<MethodInfo> methods = new List<MethodInfo>();
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;
}
}

View File

@@ -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<MethodInfo> 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<MethodInfo> 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<Type[], Type[], bool> 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<Type[], Type[], bool> isAssignableFrom = (a, b) =>
{
PropertyInfo typeArgsProperty = s_csharpInvokePropertyType.GetProperty("TypeArguments");
return ((IEnumerable<Type>)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<Type>)typeArgsProperty.GetValue(binder, null)).ToArray();
}
return null;
}
}

View File

@@ -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<byte[]> 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<byte[]> 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<int> 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<int> 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<string> DownloadString(string url, string auth, string acc)
{
return Encoding.UTF8.GetString(await DownloadBytes(url, auth, acc));
}
public async Task<string> DownloadString(string url, string auth, string acc)
{
return Encoding.UTF8.GetString(await DownloadBytes(url, auth, acc));
}
}

View File

@@ -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<ReleaseEntry> _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<ReleaseEntry> _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<byte[]> 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<byte[]> 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<int> 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<string> 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<int> 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<string> 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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<ArgumentException>(() => um.CheckForUpdates());
}
[Fact(Skip = "Consumes API Quota")]
public void CheckGithubWithNonExistingChannel()
{
// https://github.com/caesay/SquirrelCustomLauncherTestApp
using var logger = _output.BuildLoggerFor<UpdateManagerTests>();
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<ArgumentException>(() => um.CheckForUpdates());
}
[Fact]
public void NoUpdatesIfCurrentEqualsRemoteVersion()
{
using var logger = _output.BuildLoggerFor<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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<UpdateManagerTests>();
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));
}
}

View File

@@ -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<UtilityTests>();
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<int> prog = new List<int>();
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<UtilityTests>();
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<int> prog = new List<int>();
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;
}
}

View File

@@ -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<ZipPackageFile> GetLibFiles(Package package)
{
return GetFiles(package, NugetUtil.LibDirectory);
}
IEnumerable<ZipPackageFile> GetLibFiles(Package package)
{
return GetFiles(package, NugetUtil.LibDirectory);
}
IEnumerable<ZipPackageFile> GetFiles(Package package, string directory)
{
string folderPrefix = directory + Path.DirectorySeparatorChar;
return GetFiles(package).Where(file => file.Path.StartsWith(folderPrefix, StringComparison.OrdinalIgnoreCase));
}
IEnumerable<ZipPackageFile> GetFiles(Package package, string directory)
{
string folderPrefix = directory + Path.DirectorySeparatorChar;
return GetFiles(package).Where(file => file.Path.StartsWith(folderPrefix, StringComparison.OrdinalIgnoreCase));
}
List<ZipPackageFile> GetFiles(Package package)
{
return (from part in package.GetParts()
where IsPackageFile(part)
select new ZipPackageFile(part.Uri)).ToList();
}
List<ZipPackageFile> 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);
}
}