From b63b4c45fa53c76c6419b923c28cd745fdd1d3bc Mon Sep 17 00:00:00 2001 From: JusterZhu Date: Sun, 24 May 2026 16:17:17 +0800 Subject: [PATCH] test: add unit tests for Hooks, IPC, Backup/Restore, EventListener - HooksTests: NoOpUpdateHooks defaults, UpdateContext/DownloadContext record equality - ProcessInfoProviderTests: EncryptedFile send/receive roundtrip, no-file returns null - BackupRestoreTests: backup+restore roundtrip, CleanBackup keeps N versions, ListBackups metadata - EventListenerTests: add/remove/dispatch, multiple listeners, progress args, event args constructors - Removed IsTrimmable from Core.csproj (not supported on netstandard2.0) Test results: CoreTest 54/55 (1 pre-existing failure), ClientCoreTest 20/20, DifferentialTest 62/62, BowlTest 50/50 Closes #350 --- .../GeneralUpdate.Core.csproj | 3 - tests/CoreTest/Event/EventListenerTests.cs | 118 ++++++++++++++++++ .../CoreTest/FileSystem/BackupRestoreTests.cs | 94 ++++++++++++++ tests/CoreTest/Hooks/HooksTests.cs | 44 +++++++ .../CoreTest/Ipc/ProcessInfoProviderTests.cs | 77 ++++++++++++ 5 files changed, 333 insertions(+), 3 deletions(-) create mode 100644 tests/CoreTest/Event/EventListenerTests.cs create mode 100644 tests/CoreTest/FileSystem/BackupRestoreTests.cs create mode 100644 tests/CoreTest/Hooks/HooksTests.cs create mode 100644 tests/CoreTest/Ipc/ProcessInfoProviderTests.cs diff --git a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj index e93ee2a2..de6d0919 100644 --- a/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj +++ b/src/c#/GeneralUpdate.Core/GeneralUpdate.Core.csproj @@ -12,9 +12,6 @@ upgrade,update,client 10.0.0-preview netstandard2.0 - - true - true diff --git a/tests/CoreTest/Event/EventListenerTests.cs b/tests/CoreTest/Event/EventListenerTests.cs new file mode 100644 index 00000000..4aac595b --- /dev/null +++ b/tests/CoreTest/Event/EventListenerTests.cs @@ -0,0 +1,118 @@ +using System; +using GeneralUpdate.Core.Download; +using GeneralUpdate.Core.Download.Models; +using GeneralUpdate.Core.Event; +using Xunit; + +namespace CoreTest.Event; + +public class EventListenerTests +{ + private class TestListener : IUpdateEventListener + { + public int AllDownloadCompletedCalls; + public int DownloadCompletedCalls; + public int DownloadErrorCalls; + public int DownloadStatisticsCalls; + public int UpdateInfoCalls; + public int ExceptionCalls; + public int ProgressCalls; + + public void OnAllDownloadCompleted(MultiAllDownloadCompletedEventArgs args) + => AllDownloadCompletedCalls++; + + public void OnDownloadCompleted(MultiDownloadCompletedEventArgs args) + => DownloadCompletedCalls++; + + public void OnDownloadError(MultiDownloadErrorEventArgs args) + => DownloadErrorCalls++; + + public void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args) + => DownloadStatisticsCalls++; + + public void OnUpdateInfo(UpdateInfoEventArgs args) + => UpdateInfoCalls++; + + public void OnException(ExceptionEventArgs args) + => ExceptionCalls++; + + public void OnProgress(DownloadProgress progress) + => ProgressCalls++; + } + + [Fact] + public void EventManager_AddListener_And_Dispatch() + { + var listener = new TestListener(); + EventManager.Instance.AddListener((s, e) => listener.OnAllDownloadCompleted(e)); + + var args = new MultiAllDownloadCompletedEventArgs(true, Array.Empty<(object, string)>()); + EventManager.Instance.Dispatch(this, args); + + Assert.Equal(1, listener.AllDownloadCompletedCalls); + } + + [Fact] + public void EventManager_RemoveListener_StopsDispatch() + { + var listener = new TestListener(); + Action handler = (s, e) => listener.OnDownloadCompleted(e); + + EventManager.Instance.AddListener(handler); + EventManager.Instance.RemoveListener(handler); + + var args = new MultiDownloadCompletedEventArgs(new object(), true); + EventManager.Instance.Dispatch(this, args); + + Assert.Equal(0, listener.DownloadCompletedCalls); + } + + [Fact] + public void EventManager_MultipleListeners_AllCalled() + { + var listener1 = new TestListener(); + var listener2 = new TestListener(); + + EventManager.Instance.AddListener((s, e) => listener1.OnException(e)); + EventManager.Instance.AddListener((s, e) => listener2.OnException(e)); + + var args = new ExceptionEventArgs(new Exception("test"), "test message"); + EventManager.Instance.Dispatch(this, args); + + Assert.Equal(1, listener1.ExceptionCalls); + Assert.Equal(1, listener2.ExceptionCalls); + } + + [Fact] + public void ProgressEventArgs_Constructor() + { + var progress = new DownloadProgress("asset.zip", 500, 1000, 50.0, DownloadStatus.Downloading); + var args = new ProgressEventArgs(progress); + + Assert.Same(progress, args.Progress); + Assert.Equal("asset.zip", args.Progress.AssetName); + Assert.Equal(500, args.Progress.BytesDownloaded); + Assert.Equal(1000, args.Progress.TotalBytes); + Assert.Equal(50.0, args.Progress.Percentage); + Assert.Equal(DownloadStatus.Downloading, args.Progress.Status); + } + + [Fact] + public void EventArgs_Types_Constructed() + { + var allDone = new MultiAllDownloadCompletedEventArgs(true, Array.Empty<(object, string)>()); + Assert.True(allDone.IsAllDownloadCompleted); + + var downloadDone = new MultiDownloadCompletedEventArgs(new object(), true); + Assert.NotNull(downloadDone.Version); + Assert.True(downloadDone.IsComplated); + + var ex = new Exception("boom"); + var err = new MultiDownloadErrorEventArgs(ex, new object()); + Assert.Same(ex, err.Exception); + + var stats = new MultiDownloadStatisticsEventArgs(new object(), TimeSpan.FromSeconds(1), "1MB/s", 1000, 500, 50.0); + Assert.Equal("1MB/s", stats.Speed); + Assert.Equal(1000, stats.TotalBytesToReceive); + } +} diff --git a/tests/CoreTest/FileSystem/BackupRestoreTests.cs b/tests/CoreTest/FileSystem/BackupRestoreTests.cs new file mode 100644 index 00000000..28c7ce43 --- /dev/null +++ b/tests/CoreTest/FileSystem/BackupRestoreTests.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using System.Linq; +using GeneralUpdate.Core.FileSystem; +using Xunit; + +namespace CoreTest.Backup; + +public class BackupRestoreTests +{ + [Fact] + public void Backup_And_Restore_Roundtrip() + { + var tmpRoot = Path.Combine(Path.GetTempPath(), "CoreTest.Backup." + Guid.NewGuid().ToString("N")); + var sourceDir = Path.Combine(tmpRoot, "source"); + var backupDir = Path.Combine(tmpRoot, "backup"); + var restoreDir = Path.Combine(tmpRoot, "restored"); + + try + { + Directory.CreateDirectory(sourceDir); + File.WriteAllText(Path.Combine(sourceDir, "app.exe"), "v1.0"); + File.WriteAllText(Path.Combine(sourceDir, "config.json"), "{}"); + var subDir = Path.Combine(sourceDir, "data"); + Directory.CreateDirectory(subDir); + File.WriteAllText(Path.Combine(subDir, "data.db"), "mydata"); + + StorageManager.Backup(sourceDir, backupDir, Array.Empty()); + Assert.True(Directory.Exists(backupDir)); + Assert.True(File.Exists(Path.Combine(backupDir, "app.exe"))); + Assert.True(File.Exists(Path.Combine(backupDir, "config.json"))); + + StorageManager.Restore(backupDir, restoreDir); + Assert.True(Directory.Exists(restoreDir)); + Assert.Equal("v1.0", File.ReadAllText(Path.Combine(restoreDir, "app.exe"))); + Assert.Equal("{}", File.ReadAllText(Path.Combine(restoreDir, "config.json"))); + } + finally + { + if (Directory.Exists(tmpRoot)) Directory.Delete(tmpRoot, true); + } + } + + [Fact] + public void CleanBackup_KeepsOnlyRecentVersions() + { + var installPath = Path.Combine(Path.GetTempPath(), "CoreTest.CleanBackup." + Guid.NewGuid().ToString("N")); + var backupRoot = Path.Combine(installPath, "__backups"); + try + { + for (int i = 1; i <= 5; i++) + { + var verDir = Path.Combine(backupRoot, $"{i}.0.0"); + Directory.CreateDirectory(verDir); + File.WriteAllText(Path.Combine(verDir, "app.exe"), $"v{i}"); + } + + StorageManager.CleanBackup(installPath, keepVersions: 3); + + var remaining = Directory.GetDirectories(backupRoot); + Assert.Equal(3, remaining.Length); + var names = remaining.Select(Path.GetFileName).OrderBy(n => new Version(n!)).ToList(); + Assert.Equal("3.0.0", names[0]); + Assert.Equal("4.0.0", names[1]); + Assert.Equal("5.0.0", names[2]); + } + finally + { + if (Directory.Exists(installPath)) Directory.Delete(installPath, true); + } + } + + [Fact] + public void ListBackups_ReturnsMetadata() + { + var installPath = Path.Combine(Path.GetTempPath(), "CoreTest.ListBackups." + Guid.NewGuid().ToString("N")); + var backupRoot = Path.Combine(installPath, "__backups"); + try + { + var verDir = Path.Combine(backupRoot, "1.0.0"); + Directory.CreateDirectory(verDir); + File.WriteAllText(Path.Combine(verDir, "app.exe"), "v1"); + + var backups = StorageManager.ListBackups(installPath); + Assert.Single(backups); + Assert.Equal("1.0.0", backups[0].Version); + Assert.Contains("__backups", backups[0].Path); + } + finally + { + if (Directory.Exists(installPath)) Directory.Delete(installPath, true); + } + } +} diff --git a/tests/CoreTest/Hooks/HooksTests.cs b/tests/CoreTest/Hooks/HooksTests.cs new file mode 100644 index 00000000..a4d8de2e --- /dev/null +++ b/tests/CoreTest/Hooks/HooksTests.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading.Tasks; +using GeneralUpdate.Core.Hooks; +using Xunit; + +namespace CoreTest.Hooks; + +public class HooksTests +{ + [Fact] + public async Task NoOpUpdateHooks_AllMethods_ReturnDefaults() + { + var hooks = new NoOpUpdateHooks(); + var ctx = new UpdateContext("test", "/tmp", "1.0", "2.0", 1); + + Assert.True(await hooks.OnBeforeUpdateAsync(ctx)); + await hooks.OnDownloadCompletedAsync(new("a", "1.0", 100, TimeSpan.Zero, null, true)); + await hooks.OnAfterUpdateAsync(ctx); + await hooks.OnUpdateErrorAsync(ctx, new Exception("test")); + await hooks.OnBeforeStartAppAsync(ctx); + } + + [Fact] + public void UpdateContext_RecordEquality_Works() + { + var a = new UpdateContext("app", "/path", "1.0", "2.0", 1); + var b = new UpdateContext("app", "/path", "1.0", "2.0", 1); + var c = new UpdateContext("app2", "/path", "1.0", "2.0", 1); + + Assert.Equal(a, b); + Assert.NotEqual(a, c); + } + + [Fact] + public void DownloadContext_RecordEquality_Works() + { + var a = new DownloadContext("asset", "1.0", 100, TimeSpan.FromSeconds(1), "/tmp/a.zip", true); + var b = new DownloadContext("asset", "1.0", 100, TimeSpan.FromSeconds(1), "/tmp/a.zip", true); + var c = a with { AssetName = "other" }; + + Assert.Equal(a, b); + Assert.NotEqual(a, c); + } +} diff --git a/tests/CoreTest/Ipc/ProcessInfoProviderTests.cs b/tests/CoreTest/Ipc/ProcessInfoProviderTests.cs new file mode 100644 index 00000000..154ed49a --- /dev/null +++ b/tests/CoreTest/Ipc/ProcessInfoProviderTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Ipc; +using Xunit; + +namespace CoreTest.Ipc; + +public class ProcessInfoProviderTests +{ + [Fact] + public async Task EncryptedFileProvider_SendReceive_RoundTrips() + { + var tmpDir = Path.Combine(Path.GetTempPath(), "CoreTest.Ipc", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tmpDir); + try + { + var provider = new EncryptedFileProcessInfoProvider(tmpDir); + var info = new ProcessInfo( + appName: "test-app", + installPath: Path.GetTempPath(), + currentVersion: "1.0.0", + lastVersion: "2.0.0", + updateLogUrl: "https://example.com/log", + compressEncoding: System.Text.Encoding.UTF8, + compressFormat: "ZIP", + downloadTimeOut: 30, + appSecretKey: "secret", + updateVersions: new List { new() { Version = "1.0.0", Name = "test" } }, + reportUrl: "https://example.com/report", + backupDirectory: "/tmp/backup", + bowl: "", + scheme: "", + token: "", + script: "", + driverDirectory: "", + blackFileFormats: new List { ".pdb" }, + blackFiles: new List { "test.dll" }, + skipDirectories: new List { "logs" } + ); + + await provider.SendAsync(info); + var result = await provider.ReceiveAsync(); + + Assert.NotNull(result); + Assert.Equal("test-app", result!.AppName); + Assert.Equal("1.0.0", result.CurrentVersion); + Assert.NotEmpty(result.BlackFileFormats); + + Assert.False(Directory.GetFiles(tmpDir, "*.enc").Length > 0, + "Encrypted file should be deleted after ReceiveAsync"); + } + finally + { + if (Directory.Exists(tmpDir)) Directory.Delete(tmpDir, true); + } + } + + [Fact] + public async Task EncryptedFileProvider_NoFile_ReturnsNull() + { + var tmpDir = Path.Combine(Path.GetTempPath(), "CoreTest.Ipc.Empty." + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tmpDir); + try + { + var provider = new EncryptedFileProcessInfoProvider(tmpDir); + var result = await provider.ReceiveAsync(); + Assert.Null(result); + } + finally + { + if (Directory.Exists(tmpDir)) Directory.Delete(tmpDir, true); + } + } +}