Skip to content

Commit ef7fb19

Browse files
authored
Batch 9: Tests — DownloadPlanBuilder, FileTreeEnumerator, EventManager concurrency (#378)
- DownloadPlanBuilderTests: 8 tests covering empty, single, cross-version jump, frozen exclusion, forced update, version chain, same version, MinClientVersion - FileTreeEnumeratorTests: 4 tests covering flat enumeration, blacklisted format exclusion, directory skipping, non-existent directory - EventManagerConcurrencyTests: 3 tests covering concurrent add/remove/dispatch, handler exception isolation, add-remove-dispatch lifecycle All 15 new tests pass (53 existing tests also pass, 2 pre-existing failures in BackupRestoreTests and ConfiginfoBuilderTests unrelated to this batch) Closes #377
1 parent a49d77f commit ef7fb19

3 files changed

Lines changed: 270 additions & 0 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System.Linq;
2+
using GeneralUpdate.Core.Download;
3+
using GeneralUpdate.Core.Download.Models;
4+
using Xunit;
5+
6+
namespace CoreTest.Download;
7+
8+
public class DownloadPlanBuilderTests
9+
{
10+
[Fact]
11+
public void Build_EmptyAssets_ReturnsEmptyPlan()
12+
{
13+
var plan = DownloadPlanBuilder.Build(Array.Empty<DownloadAsset>(), "1.0.0");
14+
Assert.False(plan.HasAssets);
15+
}
16+
17+
[Fact]
18+
public void Build_SingleAsset_ReturnsIt()
19+
{
20+
var assets = new[] { new DownloadAsset("pkg", "http://x", 100, "sha", "1.0.1") };
21+
var plan = DownloadPlanBuilder.Build(assets, "1.0.0");
22+
Assert.True(plan.HasAssets);
23+
Assert.Single(plan.Assets);
24+
Assert.Equal("1.0.1", plan.Assets[0].Version);
25+
}
26+
27+
[Fact]
28+
public void Build_CrossVersionPackage_DirectJump()
29+
{
30+
var assets = new[]
31+
{
32+
new DownloadAsset("chain", "http://x", 100, "sha", "1.0.1"),
33+
new DownloadAsset("jump", "http://y", 500, "sha2", "2.0.0",
34+
IsCrossVersion: true, FromVersion: "1.0.0")
35+
};
36+
37+
var plan = DownloadPlanBuilder.Build(assets, "1.0.0");
38+
Assert.Single(plan.Assets); // Only the cross-version jump package
39+
Assert.Equal("2.0.0", plan.Assets[0].Version);
40+
}
41+
42+
[Fact]
43+
public void Build_FrozenPackagesExcluded()
44+
{
45+
var assets = new[]
46+
{
47+
new DownloadAsset("bad", "http://x", 100, "sha", "1.0.1", IsFreeze: true),
48+
new DownloadAsset("good", "http://y", 100, "sha2", "1.0.2")
49+
};
50+
51+
var plan = DownloadPlanBuilder.Build(assets, "1.0.0");
52+
Assert.Single(plan.Assets);
53+
Assert.Equal("1.0.2", plan.Assets[0].Version);
54+
}
55+
56+
[Fact]
57+
public void Build_ForcedUpdate_MarksPlan()
58+
{
59+
var assets = new[]
60+
{
61+
new DownloadAsset("forced", "http://x", 100, "sha", "1.0.1", IsForcibly: true)
62+
};
63+
64+
var plan = DownloadPlanBuilder.Build(assets, "1.0.0");
65+
Assert.True(plan.IsForcibly);
66+
}
67+
68+
[Fact]
69+
public void Build_VersionChain_MultipleSteps()
70+
{
71+
var assets = new[]
72+
{
73+
new DownloadAsset("v101", "http://x", 100, "sha1", "1.0.1"),
74+
new DownloadAsset("v102", "http://y", 100, "sha2", "1.0.2"),
75+
new DownloadAsset("v103", "http://z", 100, "sha3", "1.0.3")
76+
};
77+
78+
var plan = DownloadPlanBuilder.Build(assets, "1.0.0");
79+
Assert.Equal(3, plan.Assets.Count);
80+
Assert.Equal("1.0.1", plan.Assets[0].Version);
81+
Assert.Equal("1.0.3", plan.Assets[^1].Version);
82+
}
83+
84+
[Fact]
85+
public void Build_SameVersion_ReturnsEmpty()
86+
{
87+
var assets = new[] { new DownloadAsset("same", "http://x", 100, "sha", "1.0.0") };
88+
var plan = DownloadPlanBuilder.Build(assets, "1.0.0");
89+
Assert.False(plan.HasAssets);
90+
}
91+
92+
[Fact]
93+
public void Build_MinClientVersion_FiltersOut()
94+
{
95+
var assets = new[]
96+
{
97+
new DownloadAsset("compat", "http://x", 100, "sha1", "1.0.1", MinClientVersion: "1.0.0"),
98+
new DownloadAsset("incompat", "http://y", 100, "sha2", "1.0.2", MinClientVersion: "2.0.0")
99+
};
100+
101+
var plan = DownloadPlanBuilder.Build(assets, "1.0.0");
102+
Assert.Single(plan.Assets);
103+
Assert.Equal("1.0.1", plan.Assets[0].Version);
104+
}
105+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using GeneralUpdate.Core.Event;
5+
using Xunit;
6+
7+
namespace CoreTest.Event;
8+
9+
public class EventManagerConcurrencyTests
10+
{
11+
[Fact]
12+
public async Task ConcurrentAddRemoveDispatch_NoExceptions()
13+
{
14+
var completed = new TaskCompletionSource<bool>();
15+
var errors = 0;
16+
17+
// Concurrent writers
18+
Parallel.For(0, 100, i =>
19+
{
20+
try
21+
{
22+
Action<object, ExceptionEventArgs> handler = (s, e) => { };
23+
EventManager.Instance.AddListener(handler);
24+
EventManager.Instance.RemoveListener(handler);
25+
}
26+
catch { Interlocked.Increment(ref errors); }
27+
});
28+
29+
// Concurrent dispatchers
30+
Parallel.For(0, 50, i =>
31+
{
32+
try
33+
{
34+
EventManager.Instance.Dispatch(this, new ExceptionEventArgs(new Exception("test"), "test"));
35+
}
36+
catch { Interlocked.Increment(ref errors); }
37+
});
38+
39+
// Concurrent readers (dispatch + add)
40+
Parallel.For(0, 50, i =>
41+
{
42+
try
43+
{
44+
Action<object, ProgressEventArgs> handler = (s, e) => { };
45+
EventManager.Instance.AddListener(handler);
46+
EventManager.Instance.RemoveListener(handler);
47+
}
48+
catch { Interlocked.Increment(ref errors); }
49+
});
50+
51+
await Task.Delay(100); // Let all tasks finish
52+
Assert.Equal(0, errors);
53+
}
54+
55+
[Fact]
56+
public void Dispatch_HandlerException_DoesNotBlockOthers()
57+
{
58+
var handler2Called = false;
59+
Action<object, ExceptionEventArgs> handler1 = (s, e) => throw new InvalidOperationException("boom");
60+
Action<object, ExceptionEventArgs> handler2 = (s, e) => handler2Called = true;
61+
62+
EventManager.Instance.AddListener(handler1);
63+
EventManager.Instance.AddListener(handler2);
64+
65+
EventManager.Instance.Dispatch(this, new ExceptionEventArgs(null, "test"));
66+
67+
Assert.True(handler2Called);
68+
69+
// Cleanup
70+
EventManager.Instance.RemoveListener(handler1);
71+
EventManager.Instance.RemoveListener(handler2);
72+
}
73+
74+
[Fact]
75+
public void AddRemove_Dispatch_DoesNotThrow()
76+
{
77+
var callCount = 0;
78+
Action<object, ExceptionEventArgs> handler = (s, e) => Interlocked.Increment(ref callCount);
79+
80+
EventManager.Instance.AddListener(handler);
81+
EventManager.Instance.Dispatch(this, new ExceptionEventArgs(null, "test"));
82+
EventManager.Instance.RemoveListener(handler);
83+
EventManager.Instance.Dispatch(this, new ExceptionEventArgs(null, "test"));
84+
85+
Assert.Equal(1, callCount); // Only first dispatch should trigger
86+
}
87+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System.IO;
2+
using GeneralUpdate.Core.Configuration;
3+
using GeneralUpdate.Core.FileSystem;
4+
using Xunit;
5+
6+
namespace CoreTest.FileSystem;
7+
8+
public class FileTreeEnumeratorTests
9+
{
10+
[Fact]
11+
public void EnumerateFiles_ReturnsAllFilesInFlatDirectory()
12+
{
13+
var tmpDir = Path.Combine(Path.GetTempPath(), $"ft_test_{System.Guid.NewGuid():N}");
14+
try
15+
{
16+
Directory.CreateDirectory(tmpDir);
17+
File.WriteAllText(Path.Combine(tmpDir, "a.txt"), "a");
18+
File.WriteAllText(Path.Combine(tmpDir, "b.dll"), "b");
19+
20+
var enumerator = FileTreeEnumerator.FromConfig(BlackListConfig.Empty);
21+
var files = enumerator.EnumerateFiles(tmpDir).ToList();
22+
23+
Assert.Equal(2, files.Count);
24+
}
25+
finally { Directory.Delete(tmpDir, true); }
26+
}
27+
28+
[Fact]
29+
public void EnumerateFiles_BlacklistedFormat_Excluded()
30+
{
31+
var tmpDir = Path.Combine(Path.GetTempPath(), $"ft_test_{System.Guid.NewGuid():N}");
32+
try
33+
{
34+
Directory.CreateDirectory(tmpDir);
35+
File.WriteAllText(Path.Combine(tmpDir, "app.exe"), "a");
36+
File.WriteAllText(Path.Combine(tmpDir, "data.pdb"), "b");
37+
File.WriteAllText(Path.Combine(tmpDir, "config.xml"), "c");
38+
39+
var config = new BlackListConfig(BlackFormats: new[] { ".pdb", ".xml" });
40+
var enumerator = FileTreeEnumerator.FromConfig(config);
41+
var files = enumerator.EnumerateFiles(tmpDir).ToList();
42+
43+
Assert.Single(files);
44+
Assert.EndsWith(".exe", files[0]);
45+
}
46+
finally { Directory.Delete(tmpDir, true); }
47+
}
48+
49+
[Fact]
50+
public void EnumerateFiles_BlacklistedDirectory_Skipped()
51+
{
52+
var tmpDir = Path.Combine(Path.GetTempPath(), $"ft_test_{System.Guid.NewGuid():N}");
53+
try
54+
{
55+
Directory.CreateDirectory(tmpDir);
56+
var logDir = Path.Combine(tmpDir, "logs");
57+
Directory.CreateDirectory(logDir);
58+
File.WriteAllText(Path.Combine(tmpDir, "main.exe"), "a");
59+
File.WriteAllText(Path.Combine(logDir, "app.log"), "b");
60+
61+
var config = new BlackListConfig(SkipDirectorys: new[] { "logs" });
62+
var enumerator = FileTreeEnumerator.FromConfig(config);
63+
var files = enumerator.EnumerateFiles(tmpDir).ToList();
64+
65+
Assert.Single(files);
66+
Assert.EndsWith("main.exe", files[0]);
67+
}
68+
finally { Directory.Delete(tmpDir, true); }
69+
}
70+
71+
[Fact]
72+
public void EnumerateFiles_NonExistentDirectory_ReturnsEmpty()
73+
{
74+
var enumerator = FileTreeEnumerator.FromConfig(BlackListConfig.Empty);
75+
var files = enumerator.EnumerateFiles("C:\\does\\not\\exist").ToList();
76+
Assert.Empty(files);
77+
}
78+
}

0 commit comments

Comments
 (0)