From 65bd274a7d08222e81de50e4961ca38f2bb93ff6 Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Mon, 8 Jan 2024 10:58:49 +0000 Subject: [PATCH] Add test coverage for GitHub and fix JSON error --- .github/workflows/build.yml | 3 + src/Velopack.Deployment/GitHubRepository.cs | 33 ++-- .../Commands/OsxPackCommandRunner.cs | 2 +- src/Velopack.Packaging/ReleaseEntryHelper.cs | 5 + .../Commands/GitHubUploadCommand.cs | 7 +- src/Velopack.Vpk/Compat/EmbeddedRunner.cs | 5 +- src/Velopack/Internal/SimpleJson.cs | 35 +++- test/PathHelper.cs | 3 + .../DeploymentTests.cs | 159 ++++++++++++++++++ .../Velopack.Packaging.Tests.csproj | 3 + .../WindowsPackTests.cs | 19 +-- 11 files changed, 239 insertions(+), 35 deletions(-) create mode 100644 test/Velopack.Packaging.Tests/DeploymentTests.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c459bd2..d650af22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,6 +2,9 @@ name: Build on: [ push, pull_request ] +env: + VELOPACK_GITHUB_TEST_TOKEN: ${{ secrets.VELOPACK_GITHUB_TEST_TOKEN }} + jobs: build: strategy: diff --git a/src/Velopack.Deployment/GitHubRepository.cs b/src/Velopack.Deployment/GitHubRepository.cs index b48c8b5f..e73445e0 100644 --- a/src/Velopack.Deployment/GitHubRepository.cs +++ b/src/Velopack.Deployment/GitHubRepository.cs @@ -6,19 +6,16 @@ using Velopack.Sources; namespace Velopack.Deployment; -public class GitHubOptions : RepositoryOptions +public class GitHubDownloadOptions : RepositoryOptions { + public bool Prerelease { get; set; } + public string RepoUrl { get; set; } public string Token { get; set; } } -public class GitHubDownloadOptions : GitHubOptions -{ - public bool Pre { get; set; } -} - -public class GitHubUploadOptions : GitHubOptions +public class GitHubUploadOptions : GitHubDownloadOptions { public bool Publish { get; set; } @@ -33,18 +30,24 @@ public class GitHubRepository : SourceRepository !z.IsDelta).MaxBy(z => z.Version).First(); @@ -70,6 +73,7 @@ namespace Velopack.Packaging public void AddRemoteReleaseEntries(IEnumerable entries, string channel) { + channel ??= GetDefaultChannel(VelopackRuntimeInfo.SystemOs); if (!_releases.ContainsKey(channel)) _releases.Add(channel, new List()); var newEntries = entries.Where(x => !_releases[channel].Any(y => y.Version == x.Version && y.IsDelta == x.IsDelta)); @@ -78,6 +82,7 @@ namespace Velopack.Packaging public void AddNewRelease(string nupkgPath, string channel) { + channel ??= GetDefaultChannel(VelopackRuntimeInfo.SystemOs); if (!File.Exists(nupkgPath)) throw new FileNotFoundException("Could not find nupkg file", nupkgPath); diff --git a/src/Velopack.Vpk/Commands/GitHubUploadCommand.cs b/src/Velopack.Vpk/Commands/GitHubUploadCommand.cs index d035f67d..1402c648 100644 --- a/src/Velopack.Vpk/Commands/GitHubUploadCommand.cs +++ b/src/Velopack.Vpk/Commands/GitHubUploadCommand.cs @@ -6,11 +6,16 @@ public class GitHubUploadCommand : GitHubBaseCommand public string ReleaseName { get; private set; } + public bool Pre { get; private set; } + public GitHubUploadCommand() : base("github", "Upload releases to a GitHub repository.") { AddOption((v) => Publish = v, "--publish") - .SetDescription("Publish release instead of creating draft."); + .SetDescription("Create and publish instead of leaving as draft."); + + AddOption((v) => Pre = v, "--pre") + .SetDescription("Create as pre-release instead of stable."); AddOption((v) => ReleaseName = v, "--releaseName") .SetDescription("A custom name for created release.") diff --git a/src/Velopack.Vpk/Compat/EmbeddedRunner.cs b/src/Velopack.Vpk/Compat/EmbeddedRunner.cs index 0ab999db..0357de0c 100644 --- a/src/Velopack.Vpk/Compat/EmbeddedRunner.cs +++ b/src/Velopack.Vpk/Compat/EmbeddedRunner.cs @@ -62,7 +62,7 @@ public class EmbeddedRunner : ICommandRunner SigningEntitlements = command.SigningEntitlements, SigningInstallIdentity = command.SigningInstallIdentity, }; - new OsxPackCommandRunner(_logger).Releasify(options); + new OsxPackCommandRunner(_logger).Pack(options); return Task.CompletedTask; } @@ -118,7 +118,7 @@ public class EmbeddedRunner : ICommandRunner public virtual Task ExecuteGithubDownload(GitHubDownloadCommand command) { var options = new GitHubDownloadOptions { - Pre = command.Pre, + Prerelease = command.Pre, ReleaseDir = command.GetReleaseDirectory(), RepoUrl = command.RepoUrl, Token = command.Token, @@ -130,6 +130,7 @@ public class EmbeddedRunner : ICommandRunner public virtual Task ExecuteGithubUpload(GitHubUploadCommand command) { var options = new GitHubUploadOptions { + Prerelease = command.Pre, ReleaseDir = command.GetReleaseDirectory(), RepoUrl = command.RepoUrl, Token = command.Token, diff --git a/src/Velopack/Internal/SimpleJson.cs b/src/Velopack/Internal/SimpleJson.cs index 3adb2396..c4ab1f7c 100644 --- a/src/Velopack/Internal/SimpleJson.cs +++ b/src/Velopack/Internal/SimpleJson.cs @@ -67,11 +67,15 @@ using System.Reflection; using System.Runtime.Serialization; using System.Text; -#if !NET5_0_OR_GREATER +#if NET7_0_OR_GREATER +using System.Text.Json.Serialization.Metadata; +using System.Text.Json; +using System.Linq; +#else using Velopack.Json.Reflection; #endif -#if NET5_0_OR_GREATER +#if NET7_0_OR_GREATER namespace Velopack.Json { @@ -80,12 +84,35 @@ namespace Velopack.Json { public static T DeserializeObject(string json) { - return System.Text.Json.JsonSerializer.Deserialize(json); + var options = new JsonSerializerOptions { + TypeInfoResolver = new DefaultJsonTypeInfoResolver { + Modifiers = { DataMemberNameAttribute } + } + }; + return JsonSerializer.Deserialize(json, options); + } + + static void DataMemberNameAttribute(JsonTypeInfo typeInfo) + { + // https://github.com/dotnet/runtime/issues/29975 + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + foreach (JsonPropertyInfo propertyInfo in typeInfo.Properties) { + if (propertyInfo.AttributeProvider is ICustomAttributeProvider provider) { + var attr = provider.GetCustomAttributes(typeof(DataMemberAttribute), inherit: true).Cast().ToArray(); + if (attr.Length > 0) { + var a = attr[0]; + if (a.IsNameSetExplicitly) { + propertyInfo.Name = a.Name; + } + } + } + } } } } -#else +#else // ReSharper disable LoopCanBeConvertedToQuery // ReSharper disable RedundantExplicitArrayCreation diff --git a/test/PathHelper.cs b/test/PathHelper.cs index 562075e7..47228efc 100644 --- a/test/PathHelper.cs +++ b/test/PathHelper.cs @@ -15,6 +15,9 @@ public static class PathHelper public static string GetFixture(params string[] names) => Path.Combine(new string[] { GetTestRoot(), "fixtures" }.Concat(names).ToArray()); + public static string GetTestRootPath(params string[] names) + => Path.Combine(new string[] { GetTestRoot() }.Concat(names).ToArray()); + public static string GetRustSrcDir() => Path.Combine(GetProjectDir(), "src", "Rust"); diff --git a/test/Velopack.Packaging.Tests/DeploymentTests.cs b/test/Velopack.Packaging.Tests/DeploymentTests.cs new file mode 100644 index 00000000..504865fa --- /dev/null +++ b/test/Velopack.Packaging.Tests/DeploymentTests.cs @@ -0,0 +1,159 @@ +using System.Diagnostics; +using Velopack.Deployment; +using Velopack.Packaging.OSX.Commands; +using Velopack.Packaging.Windows.Commands; +using Velopack.Sources; +using Octokit; + +namespace Velopack.Packaging.Tests +{ + public class DeploymentTests + { + public readonly string GITHUB_TOKEN = Environment.GetEnvironmentVariable("VELOPACK_GITHUB_TEST_TOKEN"); + public readonly string GITHUB_REPOURL = "https://github.com/caesay/VelopackGithubUpdateTest"; + + private readonly ITestOutputHelper _output; + + public DeploymentTests(ITestOutputHelper output) + { + _output = output; + } + + private Release GetGithubRelease(GitHubClient client, string releaseName) + { + var (repoOwner, repoName) = GitHubRepository.GetOwnerAndRepo(GITHUB_REPOURL); + var existingReleases = client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult(); + return existingReleases.SingleOrDefault(s => s.Name == releaseName); + } + + [Fact] + public void CanDeployAndUpdateFromGithub() + { + using var logger = _output.BuildLoggerFor(); + var id = "GithubUpdateTest"; + var ci = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")); + using var _1 = Utility.GetTempDirectory(out var releaseDir); + var uniqueSuffix = ci ? "ci-" : "local-" + VelopackRuntimeInfo.SystemOs.GetOsShortName(); + var releaseName = $"{VelopackRuntimeInfo.VelopackDisplayVersion}-{uniqueSuffix}"; + + // delete release if already exists + var client = new GitHubClient(new ProductHeaderValue("Velopack")) { + Credentials = new Credentials(GITHUB_TOKEN) + }; + var (repoOwner, repoName) = GitHubRepository.GetOwnerAndRepo(GITHUB_REPOURL); + 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}'"); + } + + // create releases + var notesPath = Path.Combine(releaseDir, "NOTES"); + var notesContent = $""" +# Release {releaseName} +CI: {ci} +This is just a _test_! +"""; + File.WriteAllText(notesPath, notesContent); + + if (String.IsNullOrEmpty(GITHUB_TOKEN)) + throw new Exception("VELOPACK_GITHUB_TEST_TOKEN is not set."); + + PackTestApp(id, $"1.0.0-{uniqueSuffix}", "t1", releaseDir, logger, notesPath); + PackTestApp(id, $"2.0.0-{uniqueSuffix}", "t2", releaseDir, logger, notesPath); + + // 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, + }; + 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()); + + // update + var source = new GithubSource(GITHUB_REPOURL, GITHUB_TOKEN, false, logger: logger); + var releases = source.GetReleaseFeed().GetAwaiterResult(); + + var ghrel = releases.Select(r => (GithubReleaseEntry) r).ToArray(); + Assert.Equal(2, ghrel.Length); + foreach (var r in ghrel) { + Assert.Equal(releaseName, r.Release.Name); + Assert.Equal(id, r.PackageId); + Assert.Equal($"2.0.0-{uniqueSuffix}", 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), + }).GetAwaiterResult(); + + var filename = $"{id}-2.0.0-{uniqueSuffix}-{VelopackRuntimeInfo.SystemOs.GetOsShortName()}-full.nupkg"; + Assert.True(File.Exists(Path.Combine(releaseDirNew, filename))); + } + + private void PackTestApp(string id, string version, string testString, string releaseDir, ILogger logger, string releaseNotes) + { + 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}\"; }}"); + + 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); + + logger.Info($"TEST: Running {psi.FileName} {debug}"); + + using var p = Process.Start(psi); + p.WaitForExit(); + + if (p.ExitCode != 0) + throw new Exception($"dotnet publish failed with exit code {p.ExitCode}"); + + 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, + }; + var runner = new WindowsPackCommandRunner(logger); + runner.Pack(options); + } else if (VelopackRuntimeInfo.IsOSX) { + var options = new OsxPackOptions { + EntryExecutableName = "TestApp", + ReleaseDir = new DirectoryInfo(releaseDir), + PackId = id, + PackVersion = version, + PackDirectory = Path.Combine(projDir, "publish"), + ReleaseNotes = releaseNotes, + }; + var runner = new OsxPackCommandRunner(logger); + runner.Pack(options); + } else { + throw new PlatformNotSupportedException(); + } + } finally { + File.WriteAllText(testStringFile, oldText); + } + } + } +} diff --git a/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj b/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj index 16187b60..81c6eade 100644 --- a/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj +++ b/test/Velopack.Packaging.Tests/Velopack.Packaging.Tests.csproj @@ -5,7 +5,10 @@ + + + diff --git a/test/Velopack.Packaging.Tests/WindowsPackTests.cs b/test/Velopack.Packaging.Tests/WindowsPackTests.cs index c6e344fa..8f88abe4 100644 --- a/test/Velopack.Packaging.Tests/WindowsPackTests.cs +++ b/test/Velopack.Packaging.Tests/WindowsPackTests.cs @@ -623,7 +623,7 @@ public class WindowsPackTests //logger.Info($"TEST: Running {psi.FileName} {psi.ArgumentList.Aggregate((a, b) => $"{a} {b}")}"); //using var p = Process.Start(psi); - var outputfile = GetPath($"run.{RandomString(8)}.log"); + var outputfile = PathHelper.GetTestRootPath($"run.{RandomString(8)}.log"); try { // this is a huge hack, but WaitForProcess hangs in the test runner when the output is redirected @@ -684,7 +684,7 @@ public class WindowsPackTests private string RunCoveredDotnet(string exe, string[] args, string workingDir, ILogger logger, int? exitCode = 0) { - var outputfile = GetPath($"coverage.rundotnet.{RandomString(8)}.xml"); + var outputfile = PathHelper.GetTestRootPath($"coverage.rundotnet.{RandomString(8)}.xml"); if (!File.Exists(exe)) throw new Exception($"File {exe} does not exist."); @@ -730,7 +730,7 @@ public class WindowsPackTests private void PackTestApp(string id, string version, string testString, string releaseDir, ILogger logger) { - var projDir = GetPath("TestApp"); + var projDir = PathHelper.GetTestRootPath("TestApp"); var testStringFile = Path.Combine(projDir, "Const.cs"); var oldText = File.ReadAllText(testStringFile); @@ -767,17 +767,4 @@ public class WindowsPackTests File.WriteAllText(testStringFile, oldText); } } - - private static string GetPath(params string[] paths) - { - var ret = GetIntegrationTestRootDirectory(); - return (new FileInfo(paths.Aggregate(ret, Path.Combine))).FullName; - } - - private static string GetIntegrationTestRootDirectory() - { - var st = new StackFrame(true); - var di = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(st.GetFileName()), "..")); - return di.FullName; - } }