diff --git a/tests/CoreTest/Download/DownloadPlanBuilderTests.cs b/tests/CoreTest/Download/DownloadPlanBuilderTests.cs new file mode 100644 index 00000000..3661d313 --- /dev/null +++ b/tests/CoreTest/Download/DownloadPlanBuilderTests.cs @@ -0,0 +1,105 @@ +using System.Linq; +using GeneralUpdate.Core.Download; +using GeneralUpdate.Core.Download.Models; +using Xunit; + +namespace CoreTest.Download; + +public class DownloadPlanBuilderTests +{ + [Fact] + public void Build_EmptyAssets_ReturnsEmptyPlan() + { + var plan = DownloadPlanBuilder.Build(Array.Empty(), "1.0.0"); + Assert.False(plan.HasAssets); + } + + [Fact] + public void Build_SingleAsset_ReturnsIt() + { + var assets = new[] { new DownloadAsset("pkg", "http://x", 100, "sha", "1.0.1") }; + var plan = DownloadPlanBuilder.Build(assets, "1.0.0"); + Assert.True(plan.HasAssets); + Assert.Single(plan.Assets); + Assert.Equal("1.0.1", plan.Assets[0].Version); + } + + [Fact] + public void Build_CrossVersionPackage_DirectJump() + { + var assets = new[] + { + new DownloadAsset("chain", "http://x", 100, "sha", "1.0.1"), + new DownloadAsset("jump", "http://y", 500, "sha2", "2.0.0", + IsCrossVersion: true, FromVersion: "1.0.0") + }; + + var plan = DownloadPlanBuilder.Build(assets, "1.0.0"); + Assert.Single(plan.Assets); // Only the cross-version jump package + Assert.Equal("2.0.0", plan.Assets[0].Version); + } + + [Fact] + public void Build_FrozenPackagesExcluded() + { + var assets = new[] + { + new DownloadAsset("bad", "http://x", 100, "sha", "1.0.1", IsFreeze: true), + new DownloadAsset("good", "http://y", 100, "sha2", "1.0.2") + }; + + var plan = DownloadPlanBuilder.Build(assets, "1.0.0"); + Assert.Single(plan.Assets); + Assert.Equal("1.0.2", plan.Assets[0].Version); + } + + [Fact] + public void Build_ForcedUpdate_MarksPlan() + { + var assets = new[] + { + new DownloadAsset("forced", "http://x", 100, "sha", "1.0.1", IsForcibly: true) + }; + + var plan = DownloadPlanBuilder.Build(assets, "1.0.0"); + Assert.True(plan.IsForcibly); + } + + [Fact] + public void Build_VersionChain_MultipleSteps() + { + var assets = new[] + { + new DownloadAsset("v101", "http://x", 100, "sha1", "1.0.1"), + new DownloadAsset("v102", "http://y", 100, "sha2", "1.0.2"), + new DownloadAsset("v103", "http://z", 100, "sha3", "1.0.3") + }; + + var plan = DownloadPlanBuilder.Build(assets, "1.0.0"); + Assert.Equal(3, plan.Assets.Count); + Assert.Equal("1.0.1", plan.Assets[0].Version); + Assert.Equal("1.0.3", plan.Assets[^1].Version); + } + + [Fact] + public void Build_SameVersion_ReturnsEmpty() + { + var assets = new[] { new DownloadAsset("same", "http://x", 100, "sha", "1.0.0") }; + var plan = DownloadPlanBuilder.Build(assets, "1.0.0"); + Assert.False(plan.HasAssets); + } + + [Fact] + public void Build_MinClientVersion_FiltersOut() + { + var assets = new[] + { + new DownloadAsset("compat", "http://x", 100, "sha1", "1.0.1", MinClientVersion: "1.0.0"), + new DownloadAsset("incompat", "http://y", 100, "sha2", "1.0.2", MinClientVersion: "2.0.0") + }; + + var plan = DownloadPlanBuilder.Build(assets, "1.0.0"); + Assert.Single(plan.Assets); + Assert.Equal("1.0.1", plan.Assets[0].Version); + } +} diff --git a/tests/CoreTest/Event/EventManagerConcurrencyTests.cs b/tests/CoreTest/Event/EventManagerConcurrencyTests.cs new file mode 100644 index 00000000..99ff8394 --- /dev/null +++ b/tests/CoreTest/Event/EventManagerConcurrencyTests.cs @@ -0,0 +1,87 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using GeneralUpdate.Core.Event; +using Xunit; + +namespace CoreTest.Event; + +public class EventManagerConcurrencyTests +{ + [Fact] + public async Task ConcurrentAddRemoveDispatch_NoExceptions() + { + var completed = new TaskCompletionSource(); + var errors = 0; + + // Concurrent writers + Parallel.For(0, 100, i => + { + try + { + Action handler = (s, e) => { }; + EventManager.Instance.AddListener(handler); + EventManager.Instance.RemoveListener(handler); + } + catch { Interlocked.Increment(ref errors); } + }); + + // Concurrent dispatchers + Parallel.For(0, 50, i => + { + try + { + EventManager.Instance.Dispatch(this, new ExceptionEventArgs(new Exception("test"), "test")); + } + catch { Interlocked.Increment(ref errors); } + }); + + // Concurrent readers (dispatch + add) + Parallel.For(0, 50, i => + { + try + { + Action handler = (s, e) => { }; + EventManager.Instance.AddListener(handler); + EventManager.Instance.RemoveListener(handler); + } + catch { Interlocked.Increment(ref errors); } + }); + + await Task.Delay(100); // Let all tasks finish + Assert.Equal(0, errors); + } + + [Fact] + public void Dispatch_HandlerException_DoesNotBlockOthers() + { + var handler2Called = false; + Action handler1 = (s, e) => throw new InvalidOperationException("boom"); + Action handler2 = (s, e) => handler2Called = true; + + EventManager.Instance.AddListener(handler1); + EventManager.Instance.AddListener(handler2); + + EventManager.Instance.Dispatch(this, new ExceptionEventArgs(null, "test")); + + Assert.True(handler2Called); + + // Cleanup + EventManager.Instance.RemoveListener(handler1); + EventManager.Instance.RemoveListener(handler2); + } + + [Fact] + public void AddRemove_Dispatch_DoesNotThrow() + { + var callCount = 0; + Action handler = (s, e) => Interlocked.Increment(ref callCount); + + EventManager.Instance.AddListener(handler); + EventManager.Instance.Dispatch(this, new ExceptionEventArgs(null, "test")); + EventManager.Instance.RemoveListener(handler); + EventManager.Instance.Dispatch(this, new ExceptionEventArgs(null, "test")); + + Assert.Equal(1, callCount); // Only first dispatch should trigger + } +} diff --git a/tests/CoreTest/FileSystem/FileTreeEnumeratorTests.cs b/tests/CoreTest/FileSystem/FileTreeEnumeratorTests.cs new file mode 100644 index 00000000..030b7090 --- /dev/null +++ b/tests/CoreTest/FileSystem/FileTreeEnumeratorTests.cs @@ -0,0 +1,78 @@ +using System.IO; +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.FileSystem; +using Xunit; + +namespace CoreTest.FileSystem; + +public class FileTreeEnumeratorTests +{ + [Fact] + public void EnumerateFiles_ReturnsAllFilesInFlatDirectory() + { + var tmpDir = Path.Combine(Path.GetTempPath(), $"ft_test_{System.Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tmpDir); + File.WriteAllText(Path.Combine(tmpDir, "a.txt"), "a"); + File.WriteAllText(Path.Combine(tmpDir, "b.dll"), "b"); + + var enumerator = FileTreeEnumerator.FromConfig(BlackListConfig.Empty); + var files = enumerator.EnumerateFiles(tmpDir).ToList(); + + Assert.Equal(2, files.Count); + } + finally { Directory.Delete(tmpDir, true); } + } + + [Fact] + public void EnumerateFiles_BlacklistedFormat_Excluded() + { + var tmpDir = Path.Combine(Path.GetTempPath(), $"ft_test_{System.Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tmpDir); + File.WriteAllText(Path.Combine(tmpDir, "app.exe"), "a"); + File.WriteAllText(Path.Combine(tmpDir, "data.pdb"), "b"); + File.WriteAllText(Path.Combine(tmpDir, "config.xml"), "c"); + + var config = new BlackListConfig(BlackFormats: new[] { ".pdb", ".xml" }); + var enumerator = FileTreeEnumerator.FromConfig(config); + var files = enumerator.EnumerateFiles(tmpDir).ToList(); + + Assert.Single(files); + Assert.EndsWith(".exe", files[0]); + } + finally { Directory.Delete(tmpDir, true); } + } + + [Fact] + public void EnumerateFiles_BlacklistedDirectory_Skipped() + { + var tmpDir = Path.Combine(Path.GetTempPath(), $"ft_test_{System.Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tmpDir); + var logDir = Path.Combine(tmpDir, "logs"); + Directory.CreateDirectory(logDir); + File.WriteAllText(Path.Combine(tmpDir, "main.exe"), "a"); + File.WriteAllText(Path.Combine(logDir, "app.log"), "b"); + + var config = new BlackListConfig(SkipDirectorys: new[] { "logs" }); + var enumerator = FileTreeEnumerator.FromConfig(config); + var files = enumerator.EnumerateFiles(tmpDir).ToList(); + + Assert.Single(files); + Assert.EndsWith("main.exe", files[0]); + } + finally { Directory.Delete(tmpDir, true); } + } + + [Fact] + public void EnumerateFiles_NonExistentDirectory_ReturnsEmpty() + { + var enumerator = FileTreeEnumerator.FromConfig(BlackListConfig.Empty); + var files = enumerator.EnumerateFiles("C:\\does\\not\\exist").ToList(); + Assert.Empty(files); + } +}