diff --git a/src/c#/BowlTest/BowlTest.csproj b/src/c#/BowlTest/BowlTest.csproj deleted file mode 100644 index 32b4662a..00000000 --- a/src/c#/BowlTest/BowlTest.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - net10.0 - disable - enable - false - true - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - diff --git a/src/c#/DifferentialTest/Abstractions/BZip2CompressionProviderTests.cs b/src/c#/DifferentialTest/Abstractions/BZip2CompressionProviderTests.cs deleted file mode 100644 index 64495056..00000000 --- a/src/c#/DifferentialTest/Abstractions/BZip2CompressionProviderTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Binary; - -namespace DifferentialTest.Abstractions -{ - /// - /// 分支覆盖点: - /// 1. FormatVersion — 始终返回 0x00 - /// 2. CreateCompressStream — 返回 BZip2OutputStream - /// 3. CreateCompressStream — IsStreamOwner = false - /// 4. CreateDecompressStream — 返回 BZip2InputStream - /// 5. 空流/正常MemoryStream — 行为验证 - /// - /// 触发条件:正常MemoryStream - /// 预期结果:正确版本号、正确的流类型、IsStreamOwner=false - /// - public class BZip2CompressionProviderTests - { - [Fact(DisplayName = "FormatVersion_始终返回0x00")] - public void FormatVersion_Always_Returns00() - { - var provider = new BZip2CompressionProvider(); - - Assert.Equal((byte)0x00, provider.FormatVersion); - } - - [Fact(DisplayName = "CreateCompressStream_有效MemoryStream_返回BZip2OutputStream")] - public void CreateCompressStream_ValidStream_ReturnsBZip2OutputStream() - { - var provider = new BZip2CompressionProvider(); - - using var ms = new MemoryStream(); - using var stream = provider.CreateCompressStream(ms); - - Assert.NotNull(stream); - Assert.IsType(stream); - } - - [Fact(DisplayName = "CreateCompressStream_输出的BZip2OutputStream_IsStreamOwner为false")] - public void CreateCompressStream_Result_IsStreamOwnerIsFalse() - { - var provider = new BZip2CompressionProvider(); - - using var ms = new MemoryStream(); - using var stream = provider.CreateCompressStream(ms); - - var bz2Out = Assert.IsType(stream); - Assert.False(bz2Out.IsStreamOwner); - } - - [Fact(DisplayName = "CreateCompressStream_写入数据后_原始流包含数据")] - public void CreateCompressStream_WriteData_UnderlyingStreamContainsData() - { - var provider = new BZip2CompressionProvider(); - var data = new byte[] { 1, 2, 3, 4, 5, 1, 2, 3, 4, 5 }; - - using var ms = new MemoryStream(); - using (var stream = provider.CreateCompressStream(ms)) - { - stream.Write(data, 0, data.Length); - } - - Assert.True(ms.Length > 0); - } - - [Fact(DisplayName = "CreateCompressStream_释放包装流_不关闭原始流")] - public void CreateCompressStream_DisposeWrapper_DoesNotCloseUnderlying() - { - var provider = new BZip2CompressionProvider(); - - using var ms = new MemoryStream(); - using (var stream = provider.CreateCompressStream(ms)) - { - stream.WriteByte(42); - } - - Assert.True(ms.CanRead); - } - - [Fact(DisplayName = "CreateDecompressStream_有效MemoryStream_返回BZip2InputStream")] - public void CreateDecompressStream_ValidStream_ReturnsBZip2InputStream() - { - // 需要有效的BZip2数据来创建InputStream,但仅验证类型 - var provider = new BZip2CompressionProvider(); - - // 构造最小的有效BZip2数据 - using var ms = new MemoryStream(); - var minimalBz2 = new byte[] { - (byte)'B', (byte)'Z', (byte)'h', (byte)'1', - 0x31, 0x41, 0x59, 0x26, 0x53, 0x59, - 0x00, 0x00, 0x00, 0x00, 0x00, - (byte)0x17, (byte)'r', (byte)'E', (byte)'8', (byte)'P', (byte)0x90, - 0x00, 0x00, 0x00, 0x00 - }; - ms.Write(minimalBz2, 0, minimalBz2.Length); - ms.Position = 0; - - // InputStream 的构造函数会从流中读取;即便"空"也能创建(只是streamEnd=true) - var stream = provider.CreateDecompressStream(ms); - - Assert.NotNull(stream); - Assert.IsType(stream); - } - - [Fact(DisplayName = "CreateCompressStream_CancellationToken传递_不抛出异常")] - public void CreateCompressStream_WithCancellationToken_DoesNotThrow() - { - var provider = new BZip2CompressionProvider(); - using var ms = new MemoryStream(); - using var cts = new CancellationTokenSource(); - - using var stream = provider.CreateCompressStream(ms, cts.Token); - - Assert.NotNull(stream); - } - - [Fact(DisplayName = "CreateDecompressStream_CancellationToken传递_不抛出异常")] - public void CreateDecompressStream_WithCancellationToken_DoesNotThrow() - { - var provider = new BZip2CompressionProvider(); - using var ms = new MemoryStream(); - - using var stream = provider.CreateDecompressStream(ms); - - Assert.NotNull(stream); - } - } -} diff --git a/src/c#/DifferentialTest/Abstractions/BrotliCompressionProviderTests.cs b/src/c#/DifferentialTest/Abstractions/BrotliCompressionProviderTests.cs deleted file mode 100644 index 8c223aa4..00000000 --- a/src/c#/DifferentialTest/Abstractions/BrotliCompressionProviderTests.cs +++ /dev/null @@ -1,154 +0,0 @@ -#if NET6_0_OR_GREATER -using System.IO.Compression; -using GeneralUpdate.Differential.Abstractions; - -namespace DifferentialTest.Abstractions -{ - /// - /// BrotliCompressionProvider 分支覆盖测试。 - /// 覆盖:构造函数(optimalLevel true/false)、FormatVersion、CreateCompressStream、 - /// CreateDecompressStream、leaveOpen 行为、压缩解压往返。 - /// 前置条件:GeneralUpdate.Differential 需以 net6.0+ 目标框架编译, - /// 使 BrotliCompressionProvider 类型可用。 - /// - public class BrotliCompressionProviderTests - { - [Fact(DisplayName = "构造函数_默认参数_使用Optimal压缩级别")] - public void Constructor_Default_UsesOptimalCompression() - { - // Arrange & Act - var provider = new BrotliCompressionProvider(); - - // Assert - Assert.NotNull(provider); - } - - [Fact(DisplayName = "构造函数_optimalLevel为false_使用Fastest压缩级别")] - public void Constructor_OptimalLevelFalse_UsesFastestCompression() - { - // Arrange & Act - var provider = new BrotliCompressionProvider(optimalLevel: false); - - // Assert - Assert.NotNull(provider); - } - - [Fact(DisplayName = "FormatVersion_始终返回0x02")] - public void FormatVersion_Always_Returns02() - { - // Arrange - var provider = new BrotliCompressionProvider(); - - // Act - var version = provider.FormatVersion; - - // Assert - Assert.Equal((byte)0x02, version); - } - - [Fact(DisplayName = "CreateCompressStream_有效MemoryStream_返回BrotliStream")] - public void CreateCompressStream_ValidStream_ReturnsBrotliStream() - { - // Arrange - var provider = new BrotliCompressionProvider(); - using var ms = new MemoryStream(); - - // Act - using var stream = provider.CreateCompressStream(ms); - - // Assert - Assert.NotNull(stream); - var brotliStream = Assert.IsType(stream); - Assert.True(brotliStream.CanWrite); - } - - [Fact(DisplayName = "CreateCompressStream_写入数据_原始流包含压缩数据")] - public void CreateCompressStream_WriteData_UnderlyingContainsCompressedData() - { - // Arrange - var provider = new BrotliCompressionProvider(); - var data = new byte[1024]; - new Random(42).NextBytes(data); - - using var ms = new MemoryStream(); - - // Act - using (var stream = provider.CreateCompressStream(ms)) - { - stream.Write(data, 0, data.Length); - } - - // Assert - Assert.True(ms.Length > 0); - } - - [Fact(DisplayName = "CreateCompressStream_释放包装流_不关闭原始流")] - public void CreateCompressStream_Dispose_DoesNotCloseUnderlying() - { - // Arrange - var provider = new BrotliCompressionProvider(); - - using var ms = new MemoryStream(); - - // Act - using (var stream = provider.CreateCompressStream(ms)) - { - stream.WriteByte(42); - } - - // Assert: leaveOpen=true → underlying stream still readable - Assert.True(ms.CanRead); - } - - [Fact(DisplayName = "CreateDecompressStream_有效压缩数据_返回BrotliStream")] - public void CreateDecompressStream_ValidData_ReturnsBrotliStream() - { - // Arrange - var provider = new BrotliCompressionProvider(); - var data = System.Text.Encoding.UTF8.GetBytes("Hello, Brotli World!"); - - // Compress first - using var compressedMs = new MemoryStream(); - using (var compressStream = provider.CreateCompressStream(compressedMs)) - { - compressStream.Write(data, 0, data.Length); - } - - // Act - compressedMs.Position = 0; - using var decompressStream = provider.CreateDecompressStream(compressedMs); - - // Assert - Assert.NotNull(decompressStream); - Assert.IsType(decompressStream); - Assert.True(decompressStream.CanRead); - } - - [Fact(DisplayName = "压缩解压往返_产生相同数据")] - public void CompressDecompress_RoundTrip_ProducesIdenticalData() - { - // Arrange - var provider = new BrotliCompressionProvider(); - var originalData = new byte[4096]; - new Random(42).NextBytes(originalData); - - // Act — compress - using var compressedMs = new MemoryStream(); - using (var compressStream = provider.CreateCompressStream(compressedMs)) - { - compressStream.Write(originalData, 0, originalData.Length); - } - - // Act — decompress - compressedMs.Position = 0; - using var decompressStream = provider.CreateDecompressStream(compressedMs); - using var resultMs = new MemoryStream(); - decompressStream.CopyTo(resultMs); - - // Assert - var decompressedData = resultMs.ToArray(); - Assert.Equal(originalData, decompressedData); - } - } -} -#endif diff --git a/src/c#/DifferentialTest/Abstractions/DeflateCompressionProviderTests.cs b/src/c#/DifferentialTest/Abstractions/DeflateCompressionProviderTests.cs deleted file mode 100644 index 2a92530c..00000000 --- a/src/c#/DifferentialTest/Abstractions/DeflateCompressionProviderTests.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System.IO.Compression; -using GeneralUpdate.Differential.Abstractions; - -namespace DifferentialTest.Abstractions -{ - /// - /// 分支覆盖点: - /// 1. 构造函数 — optimalLevel=true → CompressionLevel.Optimal - /// 2. 构造函数 — optimalLevel=false → CompressionLevel.Fastest - /// 3. FormatVersion — 始终返回 0x01 - /// 4. CreateCompressStream — 返回 DeflateStream 包装原始流出 - /// 5. CreateCompressStream — CancellationToken默认参数 - /// 6. CreateDecompressStream — 返回 DeflateStream 包装原始流入 - /// 7. 空流/已关闭流 — 异常分支 - /// - /// 触发条件:不同构造函数参数 / 不同流状态 - /// 预期结果:正确版本号、正确流类型、leaveOpen行为正确 - /// - public class DeflateCompressionProviderTests - { - [Fact(DisplayName = "构造函数_optimalLevel为true_使用Optimal压缩级别")] - public void Constructor_OptimalLevelTrue_UsesOptimalCompression() - { - var provider = new DeflateCompressionProvider(optimalLevel: true); - - using var ms = new MemoryStream(); - using var compressStream = provider.CreateCompressStream(ms); - Assert.NotNull(compressStream); - var deflateStream = Assert.IsType(compressStream); - // 默认情况下Optimal级别无法直接读取,但可以校验流不为空 - } - - [Fact(DisplayName = "构造函数_optimalLevel为false_使用Fastest压缩级别")] - public void Constructor_OptimalLevelFalse_UsesFastestCompression() - { - var provider = new DeflateCompressionProvider(optimalLevel: false); - - using var ms = new MemoryStream(); - using var compressStream = provider.CreateCompressStream(ms); - Assert.NotNull(compressStream); - Assert.IsType(compressStream); - } - - [Fact(DisplayName = "构造函数_默认参数_使用Optimal压缩级别")] - public void Constructor_DefaultParameter_UsesOptimalCompression() - { - var provider = new DeflateCompressionProvider(); - - Assert.NotNull(provider); - } - - [Fact(DisplayName = "FormatVersion_始终返回0x01")] - public void FormatVersion_Always_Returns01() - { - var provider = new DeflateCompressionProvider(); - - Assert.Equal((byte)0x01, provider.FormatVersion); - } - - [Fact(DisplayName = "CreateCompressStream_正常MemoryStream_返回DeflateStream包装")] - public void CreateCompressStream_ValidStream_ReturnsDeflateStreamWrapper() - { - var provider = new DeflateCompressionProvider(); - - using var ms = new MemoryStream(); - using var compressStream = provider.CreateCompressStream(ms); - - Assert.NotNull(compressStream); - Assert.IsType(compressStream); - Assert.True(compressStream.CanWrite); - } - - [Fact(DisplayName = "CreateCompressStream_写入数据后_原始流包含压缩数据")] - public void CreateCompressStream_WriteData_UnderlyingStreamContainsCompressedData() - { - var provider = new DeflateCompressionProvider(); - var data = new byte[] { 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5 }; - - using var ms = new MemoryStream(); - using (var compressStream = provider.CreateCompressStream(ms)) - { - compressStream.Write(data, 0, data.Length); - } // 释放压缩流以刷新数据 - - Assert.True(ms.Length > 0); - } - - [Fact(DisplayName = "CreateCompressStream_leaveOpen为true_释放包装流不关闭原始流")] - public void CreateCompressStream_LeaveOpen_DisposingWrapperDoesNotCloseUnderlying() - { - var provider = new DeflateCompressionProvider(); - - using var ms = new MemoryStream(); - using (var compressStream = provider.CreateCompressStream(ms)) - { - compressStream.WriteByte(42); - } - - // 原始流仍可读写 - Assert.True(ms.CanRead); - } - - [Fact(DisplayName = "CreateDecompressStream_正常MemoryStream_返回DeflateStream包装")] - public void CreateDecompressStream_ValidStream_ReturnsDeflateStreamWrapper() - { - var provider = new DeflateCompressionProvider(); - - // 先压缩一些数据 - using var compressedMs = new MemoryStream(); - using (var compressStream = provider.CreateCompressStream(compressedMs)) - { - var data = System.Text.Encoding.UTF8.GetBytes("hello world hello world hello world"); - compressStream.Write(data, 0, data.Length); - } - - // 再解压 - compressedMs.Position = 0; - using var decompressStream = provider.CreateDecompressStream(compressedMs); - - Assert.NotNull(decompressStream); - Assert.IsType(decompressStream); - Assert.True(decompressStream.CanRead); - } - - [Fact(DisplayName = "CreateDecompressStream_leaveOpen为true_释放包装流不关闭原始流")] - public void CreateDecompressStream_LeaveOpen_DisposingWrapperDoesNotCloseUnderlying() - { - var provider = new DeflateCompressionProvider(); - - using var compressedMs = new MemoryStream(); - using (var compressStream = provider.CreateCompressStream(compressedMs)) - { - var data = new byte[] { 1, 2, 3, 4, 5, 1, 2, 3, 4, 5 }; - compressStream.Write(data, 0, data.Length); - } - - compressedMs.Position = 0; - using (var decompressStream = provider.CreateDecompressStream(compressedMs)) - { - var buf = new byte[10]; - int totalRead = 0; - while (totalRead < buf.Length) - { - int read = decompressStream.Read(buf, totalRead, buf.Length - totalRead); - if (read == 0) break; - totalRead += read; - } - } - - // 原始流仍可寻道 - Assert.True(compressedMs.CanSeek); - } - - [Fact(DisplayName = "CreateDecompressStream_空压缩数据_读取返回0")] - public void CreateDecompressStream_EmptyData_ReadReturnsZero() - { - var provider = new DeflateCompressionProvider(); - - using var compressedMs = new MemoryStream(); - using (var compressStream = provider.CreateCompressStream(compressedMs)) - { - // 不写入任何数据 - } - - compressedMs.Position = 0; - using var decompressStream = provider.CreateDecompressStream(compressedMs); - var buf = new byte[10]; - - // 空DeflateStream可能抛出或返回0,取决于BaseStream状态 - // 这里验证不会NullReferenceException - Assert.NotNull(decompressStream); - } - - [Fact(DisplayName = "CreateCompressStream_CancellationToken已取消_压缩流仍可创建(tokeng未被使用)")] - public void CreateCompressStream_CancellationTokenPassed_DoesNotThrow() - { - var provider = new DeflateCompressionProvider(); - using var ms = new MemoryStream(); - using var cts = new CancellationTokenSource(); - - // ICompressionProvider 接口中有 CancellationToken,但 Deflate 实现可能忽略 - using var stream = provider.CreateCompressStream(ms, cts.Token); - - Assert.NotNull(stream); - } - - [Fact(DisplayName = "最优级别和最快级别_压缩结果可能不同")] - public void OptimalVsFastest_CompressionResultsMayDiffer() - { - var optimal = new DeflateCompressionProvider(optimalLevel: true); - var fastest = new DeflateCompressionProvider(optimalLevel: false); - var data = new byte[1024]; - new Random(42).NextBytes(data); - - using var msOpt = new MemoryStream(); - using (var cs = optimal.CreateCompressStream(msOpt)) - cs.Write(data, 0, data.Length); - - using var msFast = new MemoryStream(); - using (var cs = fastest.CreateCompressStream(msFast)) - cs.Write(data, 0, data.Length); - - // 两者都能产生压缩输出(长度不一定相同,但都不应为0) - Assert.True(msOpt.Length > 0); - Assert.True(msFast.Length > 0); - } - } -} diff --git a/src/c#/DifferentialTest/Binary/BZip2InputStreamTests.cs b/src/c#/DifferentialTest/Binary/BZip2InputStreamTests.cs deleted file mode 100644 index 9240f5f0..00000000 --- a/src/c#/DifferentialTest/Binary/BZip2InputStreamTests.cs +++ /dev/null @@ -1,218 +0,0 @@ -using GeneralUpdate.Differential.Binary; - -namespace DifferentialTest.Binary -{ - /// - /// 分支覆盖点: - /// 1. CanRead/CanSeek/CanWrite — 暴露底层流属性 - /// 2. Length/Position get — 暴露底层流属性 - /// 3. Position set → NotSupportedException - /// 4. Seek → NotSupportedException - /// 5. SetLength → NotSupportedException - /// 6. Write/WriteByte → NotSupportedException - /// 7. Read — buffer为null → ArgumentNullException - /// 8. Read — 正常读取行为 - /// 9. ReadByte — streamEnd → -1 - /// 10. IsStreamOwner — 默认true, 可设置false - /// 11. Close — IsStreamOwner=false时不关闭底层流 - /// 12. Flush — 转发到底层流 - /// 13. 构造函数 — 初始化内部数组 - /// 14. 构造函数 — 空流(streamEnd立即为true) - /// - /// 触发条件:各种流状态、参数组合 - /// 预期结果:异常正确抛出、流属性正确、Close行为正确 - /// - public class BZip2InputStreamTests - { - [Fact(DisplayName = "构造函数_传入空MemoryStream_streamEnd为true")] - public void Constructor_EmptyStream_StreamEndIsTrue() - { - using var ms = new MemoryStream(); - using var stream = new BZip2InputStream(ms); - - Assert.False(stream.CanWrite); - Assert.True(stream.CanRead); - } - - [Fact(DisplayName = "构造函数_传入有效BZip2数据头_可正确初始化")] - public void Constructor_ValidBz2Header_InitializesCorrectly() - { - var bz2Data = CreateMinimalBz2Stream(); - - using var stream = new BZip2InputStream(bz2Data); - - Assert.True(stream.CanRead); - } - - [Fact(DisplayName = "CanWrite_始终返回false")] - public void CanWrite_Always_ReturnsFalse() - { - using var ms = CreateMinimalBz2Stream(); - using var stream = new BZip2InputStream(ms); - - Assert.False(stream.CanWrite); - } - - [Fact(DisplayName = "CanRead_返回底层流CanRead")] - public void CanRead_ReturnsUnderlyingStreamCanRead() - { - using var ms = new MemoryStream(); - using var stream = new BZip2InputStream(ms); - - Assert.Equal(ms.CanRead, stream.CanRead); - } - - [Fact(DisplayName = "CanSeek_返回底层流CanSeek")] - public void CanSeek_ReturnsUnderlyingStreamCanSeek() - { - using var ms = new MemoryStream(); - using var stream = new BZip2InputStream(ms); - - Assert.Equal(ms.CanSeek, stream.CanSeek); - } - - [Fact(DisplayName = "Length_返回底层流Length")] - public void Length_ReturnsUnderlyingStreamLength() - { - using var ms = new MemoryStream(new byte[100]); - using var stream = new BZip2InputStream(ms); - - Assert.Equal(ms.Length, stream.Length); - } - - [Fact(DisplayName = "Position_get_返回底层流Position")] - public void Position_Get_ReturnsUnderlyingStreamPosition() - { - using var ms = new MemoryStream(new byte[100]); - ms.Position = 10; - using var stream = new BZip2InputStream(ms); - - Assert.Equal(ms.Position, stream.Position); - } - - [Fact(DisplayName = "Position_set_抛出NotSupportedException")] - public void Position_Set_ThrowsNotSupportedException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2InputStream(ms); - - Assert.Throws(() => stream.Position = 0); - } - - [Fact(DisplayName = "Seek_抛出NotSupportedException")] - public void Seek_ThrowsNotSupportedException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2InputStream(ms); - - Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); - } - - [Fact(DisplayName = "SetLength_抛出NotSupportedException")] - public void SetLength_ThrowsNotSupportedException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2InputStream(ms); - - Assert.Throws(() => stream.SetLength(0)); - } - - [Fact(DisplayName = "Write_抛出NotSupportedException")] - public void Write_ThrowsNotSupportedException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2InputStream(ms); - - Assert.Throws(() => stream.Write(new byte[1], 0, 1)); - } - - [Fact(DisplayName = "WriteByte_抛出NotSupportedException")] - public void WriteByte_ThrowsNotSupportedException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2InputStream(ms); - - Assert.Throws(() => stream.WriteByte(0)); - } - - [Fact(DisplayName = "Read_buffer为null_抛出ArgumentNullException")] - public void Read_NullBuffer_ThrowsArgumentNullException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2InputStream(ms); - - Assert.Throws(() => stream.Read(null!, 0, 1)); - } - - [Fact(DisplayName = "Read_空流_streamEnd返回-1")] - public void Read_EmptyStream_ReturnsMinusOne() - { - using var ms = new MemoryStream(); - using var stream = new BZip2InputStream(ms); - var buffer = new byte[10]; - - int read = stream.Read(buffer, 0, buffer.Length); - - // 空流可能返回-1或触发异常 - Assert.True(read == -1 || read == 0); - } - - [Fact(DisplayName = "IsStreamOwner_默认为true_Close关闭底层流")] - public void IsStreamOwner_DefaultTrue_CloseClosesUnderlyingStream() - { - var ms = new MemoryStream(); - var stream = new BZip2InputStream(ms); - - Assert.True(stream.IsStreamOwner); - } - - [Fact(DisplayName = "IsStreamOwner_设为false_Close不关闭底层流")] - public void IsStreamOwner_SetFalse_CloseDoesNotCloseUnderlying() - { - var ms = new MemoryStream(); - var stream = new BZip2InputStream(ms) { IsStreamOwner = false }; - - stream.Close(); - - // 底层流不应被关闭(无法直接验证,但不抛出异常即可) - Assert.False(stream.IsStreamOwner); - } - - [Fact(DisplayName = "Flush_底层流存在_转发到底层流")] - public void Flush_UnderlyingStreamExists_ForwardsToUnderlying() - { - using var ms = new MemoryStream(new byte[10]); - using var stream = new BZip2InputStream(ms); - - // Flush 不应抛出异常 - stream.Flush(); - } - - [Fact(DisplayName = "构造函数_传入非BZip2数据_安全初始化")] - public void Constructor_NonBz2Data_HandlesGracefully() - { - using var ms = new MemoryStream(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); - using var stream = new BZip2InputStream(ms); - - // 构造函数应在无效数据下也不崩溃(streamEnd=true) - Assert.NotNull(stream); - } - - /// - /// 创建最小有效BZip2流 (BZh1 header + stream end marker) - /// - private static MemoryStream CreateMinimalBz2Stream() - { - var ms = new MemoryStream(); - ms.Write(new byte[] { - (byte)'B', (byte)'Z', (byte)'h', (byte)'1', - 0x31, 0x41, 0x59, 0x26, 0x53, 0x59, - 0x00, 0x00, 0x00, 0x00, 0x00, - (byte)0x17, (byte)'r', (byte)'E', (byte)'8', (byte)'P', (byte)0x90, - 0x00, 0x00, 0x00, 0x00 - }, 0, 24); - ms.Position = 0; - return ms; - } - } -} diff --git a/src/c#/DifferentialTest/Binary/BZip2OutputStreamTests.cs b/src/c#/DifferentialTest/Binary/BZip2OutputStreamTests.cs deleted file mode 100644 index 5109b66b..00000000 --- a/src/c#/DifferentialTest/Binary/BZip2OutputStreamTests.cs +++ /dev/null @@ -1,297 +0,0 @@ -using GeneralUpdate.Differential.Binary; - -namespace DifferentialTest.Binary -{ - /// - /// 分支覆盖点: - /// 1. CanRead/CanSeek — 始终返回 false - /// 2. CanWrite — 返回底层流 CanWrite - /// 3. Length/Position get — 暴露底层流属性 - /// 4. Position set → NotSupportedException - /// 5. Seek → NotSupportedException - /// 6. SetLength → NotSupportedException - /// 7. Read/ReadByte → NotSupportedException - /// 8. Write — buffer为null → ArgumentNullException - /// 9. Write — offset < 0 → ArgumentOutOfRangeException - /// 10. Write — count < 0 → ArgumentOutOfRangeException - /// 11. Write — offset+count > buffer.Length → ArgumentException - /// 12. WriteByte — run-length encoding 逻辑 - /// - currentChar == -1 → 新字符开始 - /// - currentChar == num → runLength++ (runLength > 254 分支) - /// - currentChar != num → WriteRun 重置 - /// 13. 构造函数 — blockSize clamp (blockSize > 9 / < 1) - /// 14. IsStreamOwner — 默认为true - /// 15. Close → Dispose - /// 16. BytesWritten — 累计输出字节 - /// - /// 触发条件:各种入参、流状态 - /// 预期结果:异常正确抛出、属性正确、压缩可写出 - /// - public class BZip2OutputStreamTests - { - [Fact(DisplayName = "构造函数_默认blockSize_使用blockSize=9")] - public void Constructor_DefaultBlocksize_UsesBlockSize9() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.False(stream.CanRead); - Assert.True(stream.CanWrite); - Assert.Equal(0, stream.BytesWritten); - } - - [Theory(DisplayName = "构造函数_blockSize边界值_安全处理")] - [InlineData(0, 1)] // clamp to 1 - [InlineData(1, 1)] - [InlineData(9, 9)] - [InlineData(10, 9)] // clamp to 9 - [InlineData(-1, 1)] // clamp to 1 - [InlineData(999, 9)] // clamp to 9 - public void Constructor_BlockSizeBoundaries_HandlesSafely(int input, int _) - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms, input); - - Assert.NotNull(stream); - } - - [Fact(DisplayName = "CanRead_始终返回false")] - public void CanRead_Always_ReturnsFalse() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.False(stream.CanRead); - } - - [Fact(DisplayName = "CanSeek_始终返回false")] - public void CanSeek_Always_ReturnsFalse() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.False(stream.CanSeek); - } - - [Fact(DisplayName = "CanWrite_返回底层流CanWrite")] - public void CanWrite_ReturnsUnderlyingStreamCanWrite() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.Equal(ms.CanWrite, stream.CanWrite); - } - - [Fact(DisplayName = "Length_返回底层流Length")] - public void Length_ReturnsUnderlyingStreamLength() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.Equal(ms.Length, stream.Length); - } - - [Fact(DisplayName = "Position_get_返回底层流Position")] - public void Position_Get_ReturnsUnderlyingStreamPosition() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.Equal(ms.Position, stream.Position); - } - - [Fact(DisplayName = "Position_set_抛出NotSupportedException")] - public void Position_Set_ThrowsNotSupportedException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.Throws(() => stream.Position = 0); - } - - [Fact(DisplayName = "Seek_抛出NotSupportedException")] - public void Seek_ThrowsNotSupportedException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); - } - - [Fact(DisplayName = "SetLength_抛出NotSupportedException")] - public void SetLength_ThrowsNotSupportedException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.Throws(() => stream.SetLength(0)); - } - - [Fact(DisplayName = "Read_抛出NotSupportedException")] - public void Read_ThrowsNotSupportedException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.Throws(() => stream.Read(new byte[1], 0, 1)); - } - - [Fact(DisplayName = "ReadByte_抛出NotSupportedException")] - public void ReadByte_ThrowsNotSupportedException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.Throws(() => stream.ReadByte()); - } - - [Fact(DisplayName = "Write_buffer为null_抛出ArgumentNullException")] - public void Write_NullBuffer_ThrowsArgumentNullException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - var ex = Assert.Throws(() => stream.Write(null!, 0, 1)); - Assert.Equal("buffer", ex.ParamName); - } - - [Fact(DisplayName = "Write_offset为负数_抛出ArgumentOutOfRangeException")] - public void Write_NegativeOffset_ThrowsArgumentOutOfRangeException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - var ex = Assert.Throws(() => stream.Write(new byte[10], -1, 1)); - Assert.Equal("offset", ex.ParamName); - } - - [Fact(DisplayName = "Write_count为负数_抛出ArgumentOutOfRangeException")] - public void Write_NegativeCount_ThrowsArgumentOutOfRangeException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - var ex = Assert.Throws(() => stream.Write(new byte[10], 0, -1)); - Assert.Equal("count", ex.ParamName); - } - - [Fact(DisplayName = "Write_offset+count超出范围_抛出ArgumentException")] - public void Write_OffsetPlusCountExceeds_ThrowsArgumentException() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - Assert.Throws(() => stream.Write(new byte[5], 3, 5)); - } - - [Fact(DisplayName = "WriteByte_写入单个字节_BytesWritten递增")] - public void WriteByte_SingleByte_BytesWrittenIncrements() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - stream.WriteByte(65); // 'A' - stream.WriteByte(66); // 'B' - - // BZip2有header开销,写出0字节数据时也可能有输出 - Assert.NotNull(stream); - } - - [Fact(DisplayName = "Write和Close_写入数据后Close_原始流包含数据")] - public void WriteAndClose_WriteDataThenClose_UnderlyingContainsData() - { - using var ms = new MemoryStream(); - using (var stream = new BZip2OutputStream(ms)) - { - var data = new byte[] { 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5 }; - stream.Write(data, 0, data.Length); - } - - // Close后原始流应有BZip2压缩数据 - Assert.True(ms.Length > 0); - } - - [Fact(DisplayName = "WriteByte_相同字节254次_runLength编码触发")] - public void WriteByte_SameByte254Times_RunLengthEncodingTriggered() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - for (int i = 0; i < 255; i++) - stream.WriteByte(65); - - // 应触发runLength > 254分支,WriteRun被调用 - stream.Close(); - Assert.True(ms.Length > 0); - } - - [Fact(DisplayName = "Write和Close_大数据量_能正常压缩")] - public void WriteAndClose_LargeData_CompressesNormally() - { - using var ms = new MemoryStream(); - using (var stream = new BZip2OutputStream(ms) { IsStreamOwner = false }) - { - var data = new byte[10000]; - new Random(42).NextBytes(data); - stream.Write(data, 0, data.Length); - } - - Assert.True(ms.Length > 0); - } - - [Fact(DisplayName = "Close_重复调用_不抛出异常")] - public void Close_MultipleCalls_DoesNotThrow() - { - using var ms = new MemoryStream(); - var stream = new BZip2OutputStream(ms); - - stream.WriteByte(42); - stream.Close(); - // 第二次Close/Dispose不应崩溃 - stream.Close(); - } - - [Fact(DisplayName = "IsStreamOwner_默认为true_Close关闭底层流")] - public void IsStreamOwner_DefaultTrue_CloseClosesUnderlyingStream() - { - var ms = new MemoryStream(); - var stream = new BZip2OutputStream(ms); - - Assert.True(stream.IsStreamOwner); - } - - [Fact(DisplayName = "IsStreamOwner_设为false_Close不关闭底层流")] - public void IsStreamOwner_SetFalse_CloseDoesNotCloseUnderlying() - { - var ms = new MemoryStream(); - var stream = new BZip2OutputStream(ms) { IsStreamOwner = false }; - - stream.Close(); - - Assert.False(stream.IsStreamOwner); - } - - [Fact(DisplayName = "Flush_转发到底层流")] - public void Flush_ForwardsToUnderlying() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - stream.Flush(); - // 不抛出异常即可 - } - - [Fact(DisplayName = "Write_count为0_不应有任何输出")] - public void Write_ZeroCount_DoesNotWrite() - { - using var ms = new MemoryStream(); - using var stream = new BZip2OutputStream(ms); - - var data = new byte[] { 1, 2, 3 }; - stream.Write(data, 0, 0); - - // 写count=0不应有数据输出 - Assert.True(ms.Length == 0); - } - } -} diff --git a/src/c#/DifferentialTest/Binary/StrangeCRCTests.cs b/src/c#/DifferentialTest/Binary/StrangeCRCTests.cs deleted file mode 100644 index ed70c828..00000000 --- a/src/c#/DifferentialTest/Binary/StrangeCRCTests.cs +++ /dev/null @@ -1,181 +0,0 @@ -using GeneralUpdate.Differential.Binary; - -namespace DifferentialTest.Binary -{ - /// - /// 分支覆盖点: - /// 1. 构造函数 — globalCrc = -1 (Reset调用) - /// 2. Value — 返回 ~globalCrc - /// 3. Reset — 将 globalCrc 重置为 -1 - /// 4. Update(int) — num < 0 分支 (normalize) - /// 5. Update(byte[]) — null buffer → ArgumentNullException - /// 6. Update(byte[], int, int) — offset < 0 / count < 0 / offset+count > buffer.Length → 异常分支 - /// 7. Update 循环 — 遍历 count 次 - /// 8. CRC 一致性 — 相同数据产生相同CRC - /// - /// 触发条件:各种入参 - /// 预期结果:CRC值符合预期、异常正确抛出 - /// - public class StrangeCRCTests - { - [Fact(DisplayName = "构造函数_创建实例_默认Value为0")] - public void Constructor_NewInstance_DefaultValueIsZero() - { - var crc = new StrangeCRC(); - - Assert.Equal(0L, crc.Value); - } - - [Fact(DisplayName = "Reset_重置后_Value归零")] - public void Reset_AfterUpdate_ValueReturnsToZero() - { - var crc = new StrangeCRC(); - crc.Update(42); - - crc.Reset(); - - Assert.Equal(0L, crc.Value); - } - - [Fact(DisplayName = "Update_int单值_Value变化")] - public void Update_SingleInt_ValueChanges() - { - var crc = new StrangeCRC(); - - crc.Update(65); - - Assert.NotEqual(0L, crc.Value); - } - - [Theory(DisplayName = "Update_int多个值_相同输入产生相同CRC")] - [InlineData(new int[] { 1, 2, 3, 4, 5 })] - [InlineData(new int[] { 255 })] - [InlineData(new int[] { 0 })] - [InlineData(new int[] { })] - public void Update_MultiInt_SameInputSameCrc(int[] values) - { - var crc1 = new StrangeCRC(); - foreach (var v in values) crc1.Update(v); - - var crc2 = new StrangeCRC(); - foreach (var v in values) crc2.Update(v); - - Assert.Equal(crc1.Value, crc2.Value); - } - - [Fact(DisplayName = "Update_byte数组_正确计算CRC")] - public void Update_ByteArray_ComputesCrc() - { - var crc = new StrangeCRC(); - var data = new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F }; - - crc.Update(data); - - Assert.NotEqual(0L, crc.Value); - } - - [Fact(DisplayName = "Update_byte数组null_抛出ArgumentNullException")] - public void Update_NullByteArray_ThrowsArgumentNullException() - { - var crc = new StrangeCRC(); - - var ex = Assert.Throws(() => crc.Update((byte[])null!)); - - Assert.Equal("buffer", ex.ParamName); - } - - [Fact(DisplayName = "Update_带offset和count的byte数组null_抛出ArgumentNullException")] - public void Update_OffsetCountNullBuffer_ThrowsArgumentNullException() - { - var crc = new StrangeCRC(); - - var ex = Assert.Throws(() => crc.Update(null!, 0, 1)); - - Assert.Equal("buffer", ex.ParamName); - } - - [Fact(DisplayName = "Update_offset为负数_抛出ArgumentOutOfRangeException")] - public void Update_NegativeOffset_ThrowsArgumentOutOfRangeException() - { - var crc = new StrangeCRC(); - var buffer = new byte[10]; - - var ex = Assert.Throws(() => crc.Update(buffer, -1, 1)); - - Assert.Equal("offset", ex.ParamName); - } - - [Fact(DisplayName = "Update_count为负数_抛出ArgumentOutOfRangeException")] - public void Update_NegativeCount_ThrowsArgumentOutOfRangeException() - { - var crc = new StrangeCRC(); - var buffer = new byte[10]; - - var ex = Assert.Throws(() => crc.Update(buffer, 0, -1)); - - Assert.Equal("count", ex.ParamName); - } - - [Fact(DisplayName = "Update_offset+count超出buffer长度_抛出ArgumentOutOfRangeException")] - public void Update_OffsetPlusCountExceedsLength_ThrowsArgumentOutOfRangeException() - { - var crc = new StrangeCRC(); - var buffer = new byte[5]; - - var ex = Assert.Throws(() => crc.Update(buffer, 3, 3)); - - Assert.Equal("count", ex.ParamName); - } - - [Fact(DisplayName = "Update_offset和count为0_不影响CRC值")] - public void Update_ZeroCount_DoesNotChangeCrc() - { - var crc = new StrangeCRC(); - var original = crc.Value; - var buffer = new byte[10]; - - crc.Update(buffer, 0, 0); - - Assert.Equal(original, crc.Value); - } - - [Fact(DisplayName = "Update_整个buffer与分段Update_结果一致")] - public void Update_FullBufferVsSegmented_SameResult() - { - var data = new byte[16]; - new Random(99).NextBytes(data); - - var crcFull = new StrangeCRC(); - crcFull.Update(data); - - var crcSeg = new StrangeCRC(); - crcSeg.Update(data, 0, 8); - crcSeg.Update(data, 8, 8); - - Assert.Equal(crcFull.Value, crcSeg.Value); - } - - [Fact(DisplayName = "Value_获取后仍可继续Update")] - public void Value_GetValueThenContinueUpdating_ChangesCrc() - { - var crc = new StrangeCRC(); - crc.Update(1); - var val1 = crc.Value; - crc.Update(2); - var val2 = crc.Value; - - Assert.NotEqual(val1, val2); - } - - [Fact(DisplayName = "Update_int_256边界值normalize分支覆盖")] - public void Update_IntWith256Boundary_NormalizesCorrectly() - { - var crc = new StrangeCRC(); - - for (int i = 0; i < 1000; i++) - crc.Update(i % 256); - - Assert.NotEqual(0L, crc.Value); - } - } -} diff --git a/src/c#/DifferentialTest/Differ/BsdiffDifferTests.cs b/src/c#/DifferentialTest/Differ/BsdiffDifferTests.cs deleted file mode 100644 index a7cb5ac1..00000000 --- a/src/c#/DifferentialTest/Differ/BsdiffDifferTests.cs +++ /dev/null @@ -1,207 +0,0 @@ -using Moq; -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Differ; - -namespace DifferentialTest.Binary -{ - /// - /// 分支覆盖点: - /// 1. 无参构造函数 — 使用 BZip2CompressionProvider 默认 - /// 2. 带ICompressionProvider的构造函数 — null → ArgumentNullException - /// 3. CleanAsync — CancellationToken 已取消 → OperationCanceledException - /// 4. CleanAsync — 委托到 Clean (文件系统依赖) - /// 5. DirtyAsync — CancellationToken 已取消 → OperationCanceledException - /// 6. DirtyAsync — 委托到 Dirty - /// 7. Clean/Dirty (内部) — ValidationParameters 异常分支 - /// - oldFilePath/newFilePath/patchPath 为null/空白 - /// 8. ValidationParameters 三参数所有组合 - /// - /// 触发条件:各种路径参数、CancellationToken状态 - /// 预期结果:正确异常抛出、正确委托 - /// - public class BsdiffDifferTests : IDisposable - { - private readonly string _testDir; - - public BsdiffDifferTests() - { - _testDir = Path.Combine(Path.GetTempPath(), $"BsdiffDifferTests_{Guid.NewGuid():N}"); - Directory.CreateDirectory(_testDir); - } - - public void Dispose() - { - if (Directory.Exists(_testDir)) - Directory.Delete(_testDir, true); - } - - [Fact(DisplayName = "构造函数_无参_使用BZip2CompressionProvider")] - public void Constructor_NoArgs_UsesBZip2CompressionProvider() - { - var handler = new BsdiffDiffer(); - - Assert.NotNull(handler); - } - - [Fact(DisplayName = "构造函数_自定义压缩提供者_正确创建实例")] - public void Constructor_CustomCompressionProvider_CreatesInstance() - { - var mockProvider = new Mock(); - mockProvider.Setup(p => p.FormatVersion).Returns(0x01); - - var handler = new BsdiffDiffer(mockProvider.Object); - - Assert.NotNull(handler); - } - - [Fact(DisplayName = "构造函数_compressionProvider为null_抛出ArgumentNullException")] - public void Constructor_NullProvider_ThrowsArgumentNullException() - { - var ex = Assert.Throws(() => new BsdiffDiffer(null!)); - - Assert.Equal("compressionProvider", ex.ParamName); - } - - [Fact(DisplayName = "CleanAsync_CancellationToken已取消_抛出OperationCanceledException")] - public async Task CleanAsync_CancelledToken_ThrowsOperationCanceledException() - { - var handler = new BsdiffDiffer(); - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - await Assert.ThrowsAsync(() => - handler.CleanAsync("old", "new", "patch", cts.Token)); - } - - [Fact(DisplayName = "DirtyAsync_CancellationToken已取消_抛出OperationCanceledException")] - public async Task DirtyAsync_CancelledToken_ThrowsOperationCanceledException() - { - var handler = new BsdiffDiffer(); - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - await Assert.ThrowsAsync(() => - handler.DirtyAsync("old", "new", "patch", cts.Token)); - } - - [Fact(DisplayName = "Clean_文件不存在_正常失败")] - public async Task Clean_NonExistentFiles_ThrowsFileNotFound() - { - var handler = new BsdiffDiffer(); - - await Assert.ThrowsAsync(() => - handler.Clean( - Path.Combine(_testDir, "nonexistent_old.bin"), - Path.Combine(_testDir, "nonexistent_new.bin"), - Path.Combine(_testDir, "output.patch"))); - } - - [Fact(DisplayName = "Dirty_文件不存在_正常失败")] - public async Task Dirty_NonExistentFiles_ThrowsFileNotFound() - { - var handler = new BsdiffDiffer(); - - await Assert.ThrowsAsync(() => - handler.Dirty( - Path.Combine(_testDir, "nonexistent_old.bin"), - Path.Combine(_testDir, "nonexistent_new.bin"), - Path.Combine(_testDir, "nonexistent_patch.bin"))); - } - - [Fact(DisplayName = "Clean_源和目标文件相同_生成最小补丁")] - public async Task Clean_IdenticalFiles_GeneratesMinimalPatch() - { - var handler = new BsdiffDiffer(); - var oldFile = Path.Combine(_testDir, "old.bin"); - var newFile = Path.Combine(_testDir, "new.bin"); - var patchFile = Path.Combine(_testDir, "patch.bin"); - - var data = new byte[1024]; - new Random(42).NextBytes(data); - File.WriteAllBytes(oldFile, data); - File.WriteAllBytes(newFile, data); - - await handler.Clean(oldFile, newFile, patchFile); - - Assert.True(File.Exists(patchFile)); - Assert.True(new FileInfo(patchFile).Length > 0); - } - - [Fact(DisplayName = "Clean_生成补丁后Dirty还原_文件内容一致")] - public async Task CleanThenDirty_RoundTrip_ProducesIdenticalFile() - { - var handler = new BsdiffDiffer(); - var oldFile = Path.Combine(_testDir, "old.bin"); - var newFile = Path.Combine(_testDir, "new.bin"); - var patchedFile = Path.Combine(_testDir, "patched.bin"); - var patchFile = Path.Combine(_testDir, "patch.bin"); - - var oldData = new byte[4096]; - new Random(77).NextBytes(oldData); - File.WriteAllBytes(oldFile, oldData); - - var newData = new byte[4096]; - Array.Copy(oldData, newData, 2048); // 前半相同 - var suffix = new byte[2048]; new Random(88).NextBytes(suffix); Array.Copy(suffix, 0, newData, 2048, 2048); // 后半不同 - File.WriteAllBytes(newFile, newData); - - // Clean: 生成补丁 - await handler.Clean(oldFile, newFile, patchFile); - Assert.True(File.Exists(patchFile)); - - // Dirty: 应用补丁 - await handler.Dirty(oldFile, patchedFile, patchFile); - Assert.True(File.Exists(patchedFile)); - - // 验证 - var resultData = File.ReadAllBytes(patchedFile); - Assert.Equal(newData, resultData); - } - - [Fact(DisplayName = "Clean_空文件_能正确生成补丁")] - public async Task Clean_EmptyFiles_GeneratesPatch() - { - var handler = new BsdiffDiffer(); - var oldFile = Path.Combine(_testDir, "empty_old.bin"); - var newFile = Path.Combine(_testDir, "empty_new.bin"); - var patchFile = Path.Combine(_testDir, "empty_patch.bin"); - - File.WriteAllBytes(oldFile, Array.Empty()); - File.WriteAllBytes(newFile, Array.Empty()); - - await handler.Clean(oldFile, newFile, patchFile); - - Assert.True(File.Exists(patchFile)); - } - - [Fact(DisplayName = "Dirty_旧文件空_应用补丁生成新文件")] - public async Task Dirty_EmptyOldFile_AppliesPatchToNewFile() - { - var handler = new BsdiffDiffer(); - var oldFile = Path.Combine(_testDir, "empty_old.bin"); - var newFile = Path.Combine(_testDir, "full_new.bin"); - var patchedFile = Path.Combine(_testDir, "patched.bin"); - var patchFile = Path.Combine(_testDir, "patch.bin"); - - File.WriteAllBytes(oldFile, Array.Empty()); - var newData = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; - File.WriteAllBytes(newFile, newData); - - await handler.Clean(oldFile, newFile, patchFile); - await handler.Dirty(oldFile, patchedFile, patchFile); - - Assert.Equal(newData, File.ReadAllBytes(patchedFile)); - } - - [Fact(DisplayName = "Dirty_不存在的补丁文件_正常抛出异常")] - public async Task Dirty_NonExistentPatch_Throws() - { - var handler = new BsdiffDiffer(); - var oldFile = Path.Combine(_testDir, "old.bin"); - File.WriteAllBytes(oldFile, new byte[] { 1, 2, 3 }); - - await Assert.ThrowsAsync(() => - handler.Dirty(oldFile, Path.Combine(_testDir, "new.bin"), Path.Combine(_testDir, "nonexistent.patch"))); - } - } -} diff --git a/src/c#/DifferentialTest/Differ/StreamingHdiffDifferTests.cs b/src/c#/DifferentialTest/Differ/StreamingHdiffDifferTests.cs deleted file mode 100644 index d734d49d..00000000 --- a/src/c#/DifferentialTest/Differ/StreamingHdiffDifferTests.cs +++ /dev/null @@ -1,469 +0,0 @@ -using Moq; -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Differential.Differ; - -namespace DifferentialTest.Differ -{ - /// - /// StreamingHdiffDiffer 单元测试。 - /// - /// 覆盖点: - /// 1. 无参构造函数 — 使用 DeflateCompressionProvider 默认值 - /// 2. 有参构造函数 — 自定义压缩提供者和参数 - /// 3. 构造函数 — null provider → ArgumentNullException - /// 4. 构造函数 — blockSize≤0 → ArgumentOutOfRangeException - /// 5. 构造函数 — maxWindowSize≤0 → ArgumentOutOfRangeException - /// 6. CleanAsync — null/空路径 → ArgumentNullException - /// 7. CleanAsync — CancellationToken已取消 → OperationCanceledException - /// 8. CleanAsync — 文件不存在 → FileNotFoundException - /// 9. Clean — 相同文件生成最小补丁 - /// 10. Clean — 不同文件生成补丁 - /// 11. Clean — 空文件正确处理 - /// 12. CleanThenDirty — 相同文件往返验证 - /// 13. CleanThenDirty — 修改文件往返验证 - /// 14. CleanThenDirty — 完全不同文件往返验证 - /// 15. CleanThenDirty — 空旧文件生成新文件 - /// 16. DirtyAsync — null路径 → ArgumentNullException - /// 17. DirtyAsync — CancellationToken已取消 → OperationCanceledException - /// 18. Dirty — 不存在的补丁文件 → 抛出异常 - /// 19. 自定义BlockSize — 功能验证往返 - /// - public class StreamingHdiffDifferTests : IDisposable - { - private readonly string _testDir; - - public StreamingHdiffDifferTests() - { - _testDir = Path.Combine(Path.GetTempPath(), $"StreamingHdiffDifferTests_{Guid.NewGuid():N}"); - Directory.CreateDirectory(_testDir); - } - - public void Dispose() - { - if (Directory.Exists(_testDir)) - Directory.Delete(_testDir, true); - } - - #region Constructor Tests - - [Fact(DisplayName = "无参构造函数_使用默认DeflateCompressionProvider和默认参数")] - public void Constructor_NoArgs_UsesDefaults() - { - // Arrange & Act - var differ = new StreamingHdiffDiffer(); - - // Assert - Assert.NotNull(differ); - Assert.Equal(64 * 1024, differ.BlockSize); - Assert.Equal(128 * 1024 * 1024, differ.MaxWindowSize); - } - - [Fact(DisplayName = "有参构造函数_自定义参数_正确创建实例")] - public void Constructor_ValidParams_CreatesInstance() - { - // Arrange - var mockProvider = new Mock(); - mockProvider.Setup(p => p.FormatVersion).Returns(0x01); - const int blockSize = 32 * 1024; - const int maxWindowSize = 64 * 1024 * 1024; - - // Act - var differ = new StreamingHdiffDiffer(mockProvider.Object, blockSize, maxWindowSize); - - // Assert - Assert.NotNull(differ); - Assert.Equal(blockSize, differ.BlockSize); - Assert.Equal(maxWindowSize, differ.MaxWindowSize); - } - - [Fact(DisplayName = "构造函数_compressionProvider为null_抛出ArgumentNullException")] - public void Constructor_NullProvider_ThrowsArgumentNullException() - { - // Arrange, Act & Assert - var ex = Assert.Throws(() => - new StreamingHdiffDiffer(null!, 64 * 1024, 128 * 1024 * 1024)); - - Assert.Equal("compressionProvider", ex.ParamName); - } - - [Fact(DisplayName = "构造函数_blockSize为零_抛出ArgumentOutOfRangeException")] - public void Constructor_ZeroBlockSize_ThrowsArgumentOutOfRangeException() - { - // Arrange - var mockProvider = new Mock(); - mockProvider.Setup(p => p.FormatVersion).Returns(0x01); - - // Act & Assert - var ex = Assert.Throws(() => - new StreamingHdiffDiffer(mockProvider.Object, 0, 128 * 1024 * 1024)); - - Assert.Equal("blockSize", ex.ParamName); - } - - [Fact(DisplayName = "构造函数_maxWindowSize为负数_抛出ArgumentOutOfRangeException")] - public void Constructor_NegativeMaxWindowSize_ThrowsArgumentOutOfRangeException() - { - // Arrange - var mockProvider = new Mock(); - mockProvider.Setup(p => p.FormatVersion).Returns(0x01); - - // Act & Assert - var ex = Assert.Throws(() => - new StreamingHdiffDiffer(mockProvider.Object, 64 * 1024, -1)); - - Assert.Equal("maxWindowSize", ex.ParamName); - } - - #endregion Constructor Tests - - #region CleanAsync Tests - - [Fact(DisplayName = "CleanAsync_oldFilePath为null_抛出ArgumentNullException")] - public async Task CleanAsync_NullOldPath_ThrowsArgumentNullException() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - var newPath = Path.Combine(_testDir, "new.bin"); - var patchPath = Path.Combine(_testDir, "patch.bin"); - - // Act & Assert - await Assert.ThrowsAsync(() => - differ.CleanAsync(null!, newPath, patchPath)); - } - - [Fact(DisplayName = "CleanAsync_newFilePath为空白字符串_抛出ArgumentNullException")] - public async Task CleanAsync_WhitespaceNewPath_ThrowsArgumentNullException() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - var oldPath = Path.Combine(_testDir, "old.bin"); - var patchPath = Path.Combine(_testDir, "patch.bin"); - - // Act & Assert - await Assert.ThrowsAsync(() => - differ.CleanAsync(oldPath, " ", patchPath)); - } - - [Fact(DisplayName = "CleanAsync_CancellationToken已取消_抛出OperationCanceledException")] - public async Task CleanAsync_CancelledToken_ThrowsOperationCanceledException() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - // Act & Assert - await Assert.ThrowsAsync(() => - differ.CleanAsync("old", "new", "patch", cts.Token)); - } - - [Fact(DisplayName = "Clean_文件不存在_抛出FileNotFoundException")] - public async Task Clean_NonExistentFiles_Throws() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - - // Act & Assert - await Assert.ThrowsAsync(() => - differ.CleanAsync( - Path.Combine(_testDir, "nonexistent_old.bin"), - Path.Combine(_testDir, "nonexistent_new.bin"), - Path.Combine(_testDir, "output.patch"))); - } - - [Fact(DisplayName = "Clean_相同文件_生成最小补丁")] - public async Task Clean_IdenticalFiles_GeneratesMinimalPatch() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - var oldFile = Path.Combine(_testDir, "old.bin"); - var newFile = Path.Combine(_testDir, "new.bin"); - var patchFile = Path.Combine(_testDir, "patch.bin"); - - var data = new byte[2048]; - new Random(42).NextBytes(data); - File.WriteAllBytes(oldFile, data); - File.WriteAllBytes(newFile, data); - - // Act - await differ.CleanAsync(oldFile, newFile, patchFile); - - // Assert - Assert.True(File.Exists(patchFile)); - Assert.True(new FileInfo(patchFile).Length > 0, "补丁文件不应为空"); - } - - [Fact(DisplayName = "Clean_后半部分不同的文件_生成补丁")] - public async Task Clean_DifferentFiles_GeneratesPatch() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - var oldFile = Path.Combine(_testDir, "old.bin"); - var newFile = Path.Combine(_testDir, "new.bin"); - var patchFile = Path.Combine(_testDir, "patch.bin"); - - // 4KB 文件,前半相同,后半不同 - var oldData = new byte[4096]; - new Random(100).NextBytes(oldData); - File.WriteAllBytes(oldFile, oldData); - - var newData = new byte[4096]; - Array.Copy(oldData, 0, newData, 0, 2048); // 前半相同 - var suffix = new byte[2048]; - new Random(200).NextBytes(suffix); - Array.Copy(suffix, 0, newData, 2048, 2048); // 后半不同 - File.WriteAllBytes(newFile, newData); - - // Act - await differ.CleanAsync(oldFile, newFile, patchFile); - - // Assert - Assert.True(File.Exists(patchFile)); - Assert.True(new FileInfo(patchFile).Length > 0, "补丁文件不应为空"); - } - - [Fact(DisplayName = "Clean_空文件_正常处理")] - public async Task Clean_EmptyFiles_HandlesGracefully() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - var oldFile = Path.Combine(_testDir, "empty_old.bin"); - var newFile = Path.Combine(_testDir, "empty_new.bin"); - var patchFile = Path.Combine(_testDir, "empty_patch.bin"); - - File.WriteAllBytes(oldFile, Array.Empty()); - File.WriteAllBytes(newFile, Array.Empty()); - - // Act - await differ.CleanAsync(oldFile, newFile, patchFile); - - // Assert - Assert.True(File.Exists(patchFile)); - Assert.True(new FileInfo(patchFile).Length > 0, "即使空文件也应生成有效的BSDIF补丁头"); - } - - #endregion CleanAsync Tests - - #region CleanThenDirty Round-Trip Tests - - [Fact(DisplayName = "CleanThenDirty_相同文件_生成与源文件一致的结果")] - public async Task CleanThenDirty_IdenticalFiles_ProducesIdenticalOutput() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - var oldFile = Path.Combine(_testDir, "old.bin"); - var newFile = Path.Combine(_testDir, "new.bin"); - var patchedFile = Path.Combine(_testDir, "patched.bin"); - var patchFile = Path.Combine(_testDir, "patch.bin"); - - var data = new byte[4096]; - new Random(77).NextBytes(data); - File.WriteAllBytes(oldFile, data); - File.WriteAllBytes(newFile, data); - - // Act — Clean: 生成补丁 - await differ.CleanAsync(oldFile, newFile, patchFile); - Assert.True(File.Exists(patchFile)); - - // Act — Dirty: 应用补丁 - await differ.DirtyAsync(oldFile, patchedFile, patchFile); - Assert.True(File.Exists(patchedFile)); - - // Assert - var resultData = File.ReadAllBytes(patchedFile); - Assert.Equal(data, resultData); - } - - [Fact(DisplayName = "CleanThenDirty_部分修改文件_生成正确结果")] - public async Task CleanThenDirty_ModifiedFiles_ProducesExpectedOutput() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - var oldFile = Path.Combine(_testDir, "old.bin"); - var newFile = Path.Combine(_testDir, "new.bin"); - var patchedFile = Path.Combine(_testDir, "patched.bin"); - var patchFile = Path.Combine(_testDir, "patch.bin"); - - // 4KB 旧文件,新文件前半2KB相同、后半2KB不同 - var oldData = new byte[4096]; - new Random(88).NextBytes(oldData); - File.WriteAllBytes(oldFile, oldData); - - var newData = new byte[4096]; - Array.Copy(oldData, 0, newData, 0, 2048); // 前半相同 - var suffix = new byte[2048]; - new Random(99).NextBytes(suffix); - Array.Copy(suffix, 0, newData, 2048, 2048); // 后半不同 - File.WriteAllBytes(newFile, newData); - - // Act — Clean - await differ.CleanAsync(oldFile, newFile, patchFile); - Assert.True(File.Exists(patchFile)); - - // Act — Dirty - await differ.DirtyAsync(oldFile, patchedFile, patchFile); - Assert.True(File.Exists(patchedFile)); - - // Assert - var resultData = File.ReadAllBytes(patchedFile); - Assert.Equal(newData, resultData); - } - - [Fact(DisplayName = "CleanThenDirty_完全不同文件_生成正确结果")] - public async Task CleanThenDirty_CompletelyDifferentFiles_ProducesExpectedOutput() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - var oldFile = Path.Combine(_testDir, "old.bin"); - var newFile = Path.Combine(_testDir, "new.bin"); - var patchedFile = Path.Combine(_testDir, "patched.bin"); - var patchFile = Path.Combine(_testDir, "patch.bin"); - - var oldData = new byte[4096]; - new Random(111).NextBytes(oldData); - File.WriteAllBytes(oldFile, oldData); - - var newData = new byte[4096]; - new Random(222).NextBytes(newData); - File.WriteAllBytes(newFile, newData); - - // Act — Clean - await differ.CleanAsync(oldFile, newFile, patchFile); - Assert.True(File.Exists(patchFile)); - - // Act — Dirty - await differ.DirtyAsync(oldFile, patchedFile, patchFile); - Assert.True(File.Exists(patchedFile)); - - // Assert - var resultData = File.ReadAllBytes(patchedFile); - Assert.Equal(newData, resultData); - } - - [Fact(DisplayName = "CleanThenDirty_空旧文件_正确生成新文件")] - public async Task CleanThenDirty_EmptyOldFile_ProducesNewFile() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - var oldFile = Path.Combine(_testDir, "empty_old.bin"); - var newFile = Path.Combine(_testDir, "new.bin"); - var patchedFile = Path.Combine(_testDir, "patched.bin"); - var patchFile = Path.Combine(_testDir, "patch.bin"); - - File.WriteAllBytes(oldFile, Array.Empty()); - var newData = new byte[512]; - new Random(333).NextBytes(newData); - File.WriteAllBytes(newFile, newData); - - // Act — Clean - await differ.CleanAsync(oldFile, newFile, patchFile); - Assert.True(File.Exists(patchFile)); - - // Act — Dirty - await differ.DirtyAsync(oldFile, patchedFile, patchFile); - Assert.True(File.Exists(patchedFile)); - - // Assert - var resultData = File.ReadAllBytes(patchedFile); - Assert.Equal(newData, resultData); - } - - #endregion CleanThenDirty Round-Trip Tests - - #region DirtyAsync Tests - - [Fact(DisplayName = "DirtyAsync_oldFilePath为null_抛出ArgumentNullException")] - public async Task DirtyAsync_NullOldPath_ThrowsArgumentNullException() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - var newPath = Path.Combine(_testDir, "new.bin"); - var patchPath = Path.Combine(_testDir, "patch.bin"); - File.WriteAllBytes(patchPath, new byte[] { 1, 2, 3 }); - - // Act & Assert - await Assert.ThrowsAsync(() => - differ.DirtyAsync(null!, newPath, patchPath)); - } - - [Fact(DisplayName = "DirtyAsync_CancellationToken已取消_抛出OperationCanceledException")] - public async Task DirtyAsync_CancelledToken_ThrowsOperationCanceledException() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - // Act & Assert - await Assert.ThrowsAsync(() => - differ.DirtyAsync("old", "new", "patch", cts.Token)); - } - - [Fact(DisplayName = "Dirty_不存在的补丁文件_抛出异常")] - public async Task Dirty_NonExistentPatch_Throws() - { - // Arrange - var differ = new StreamingHdiffDiffer(); - var oldFile = Path.Combine(_testDir, "old.bin"); - var newFile = Path.Combine(_testDir, "new.bin"); - var patchFile = Path.Combine(_testDir, "nonexistent.patch"); - - File.WriteAllBytes(oldFile, new byte[] { 1, 2, 3, 4, 5 }); - - // Act & Assert - await Assert.ThrowsAsync(() => - differ.DirtyAsync(oldFile, newFile, patchFile)); - } - - #endregion DirtyAsync Tests - - #region Custom Configuration Tests - - [Fact(DisplayName = "自定义BlockSize_16KB_往返功能正常")] - public async Task Constructor_CustomBlockSize_Functional() - { - // Arrange - var mockProvider = new Mock(); - mockProvider.Setup(p => p.FormatVersion).Returns(0x01); - mockProvider - .Setup(p => p.CreateCompressStream(It.IsAny(), It.IsAny())) - .Returns((s, _) => new System.IO.Compression.DeflateStream(s, System.IO.Compression.CompressionLevel.Fastest, true)); - mockProvider - .Setup(p => p.CreateDecompressStream(It.IsAny(), It.IsAny())) - .Returns((s, _) => new System.IO.Compression.DeflateStream(s, System.IO.Compression.CompressionMode.Decompress, true)); - - const int blockSize = 16 * 1024; - var differ = new StreamingHdiffDiffer(mockProvider.Object, blockSize, 128 * 1024 * 1024); - var oldFile = Path.Combine(_testDir, "old.bin"); - var newFile = Path.Combine(_testDir, "new.bin"); - var patchedFile = Path.Combine(_testDir, "patched.bin"); - var patchFile = Path.Combine(_testDir, "patch.bin"); - - // 创建足够大的文件测试16KB分块哈希 - var oldData = new byte[32768]; - new Random(444).NextBytes(oldData); - File.WriteAllBytes(oldFile, oldData); - - var newData = new byte[32768]; - Array.Copy(oldData, 0, newData, 0, 16384); // 前半相同 - var suffix = new byte[16384]; - new Random(555).NextBytes(suffix); - Array.Copy(suffix, 0, newData, 16384, 16384); // 后半不同 - File.WriteAllBytes(newFile, newData); - - // Act — Clean - await differ.CleanAsync(oldFile, newFile, patchFile); - Assert.True(File.Exists(patchFile)); - - // Act — Dirty - await differ.DirtyAsync(oldFile, patchedFile, patchFile); - Assert.True(File.Exists(patchedFile)); - - // Assert - var resultData = File.ReadAllBytes(patchedFile); - Assert.Equal(newData, resultData); - } - - #endregion Custom Configuration Tests - } -} diff --git a/src/c#/DifferentialTest/DifferentialTest.csproj b/src/c#/DifferentialTest/DifferentialTest.csproj deleted file mode 100644 index 1011e8a9..00000000 --- a/src/c#/DifferentialTest/DifferentialTest.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - net10.0 - enable - enable - false - true - default - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - diff --git a/src/c#/DifferentialTest/GlobalUsings.cs b/src/c#/DifferentialTest/GlobalUsings.cs deleted file mode 100644 index 66c75e8d..00000000 --- a/src/c#/DifferentialTest/GlobalUsings.cs +++ /dev/null @@ -1,8 +0,0 @@ -global using System; -global using System.Collections.Generic; -global using System.IO; -global using System.Linq; -global using System.Net.Http; -global using System.Threading; -global using System.Threading.Tasks; -global using Xunit; diff --git a/src/c#/DifferentialTest/Matchers/DefaultCleanMatcherTests.cs b/src/c#/DifferentialTest/Matchers/DefaultCleanMatcherTests.cs deleted file mode 100644 index c321e347..00000000 --- a/src/c#/DifferentialTest/Matchers/DefaultCleanMatcherTests.cs +++ /dev/null @@ -1,192 +0,0 @@ -using GeneralUpdate.Core.Differential; - -namespace DifferentialTest.Matchers -{ - /// - /// 分支覆盖点: - /// 1. Match — oldFile匹配成功 (名称+相对路径匹配, 且文件存在) - /// 2. Match — oldFile名称不匹配 → null - /// 3. Match — oldFile相对路径不匹配 → null - /// 4. Match — oldFile匹配但文件不存在(File.Exists=false) → null - /// 5. Match — newFile匹配但文件不存在(File.Exists=false) → null - /// 6. Match — leftNodes为空 → null - /// 7. Match — leftNodes有多个匹配 → 取FirstOrDefault - /// 8. Match — 大小写不敏感匹配 - /// - /// 触发条件:各种 FileNode 组合 - /// 预期结果:正确匹配或返回null - /// - public class DefaultCleanMatcherTests : IDisposable - { - private readonly string _testDir; - - public DefaultCleanMatcherTests() - { - _testDir = Path.Combine(Path.GetTempPath(), $"CleanMatcherTests_{Guid.NewGuid():N}"); - Directory.CreateDirectory(_testDir); - } - - public void Dispose() - { - if (Directory.Exists(_testDir)) - Directory.Delete(_testDir, true); - } - - [Fact(DisplayName = "Match_名称和相对路径均匹配且文件存在_返回oldFile")] - public void Match_NameAndPathMatchFileExists_ReturnsOldFile() - { - var matcher = new DefaultCleanMatcher(); - - var oldFile = CreateFileNode("app.dll", "sub"); - var newFile = CreateFileNode("app.dll", "sub"); - var leftNodes = new List { oldFile }; - - var result = matcher.Match(newFile, leftNodes); - - Assert.NotNull(result); - Assert.Equal(oldFile.FullName, result.FullName); - } - - [Fact(DisplayName = "Match_名称匹配但相对路径不匹配_返回null")] - public void Match_NameMatchButPathMismatch_ReturnsNull() - { - var matcher = new DefaultCleanMatcher(); - - var oldFile = CreateFileNode("app.dll", "sub1"); - var newFile = CreateFileNode("app.dll", "sub2"); - var leftNodes = new List { oldFile }; - - var result = matcher.Match(newFile, leftNodes); - - Assert.Null(result); - } - - [Fact(DisplayName = "Match_相对路径匹配但名称不匹配_返回null")] - public void Match_PathMatchButNameMismatch_ReturnsNull() - { - var matcher = new DefaultCleanMatcher(); - - var oldFile = CreateFileNode("old.dll", "sub"); - var newFile = CreateFileNode("new.dll", "sub"); - var leftNodes = new List { oldFile }; - - var result = matcher.Match(newFile, leftNodes); - - Assert.Null(result); - } - - [Fact(DisplayName = "Match_匹配但oldFile文件不存在_返回null")] - public void Match_FoundButOldFileNotExist_ReturnsNull() - { - var matcher = new DefaultCleanMatcher(); - var nonExistent = Path.Combine(_testDir, "nonexistent.dll"); - - var oldFile = new GeneralUpdate.Core.FileSystem.FileNode - { - Name = "app.dll", - RelativePath = "sub", - FullName = nonExistent - }; - var newFile = CreateFileNode("app.dll", "sub"); - var leftNodes = new List { oldFile }; - - var result = matcher.Match(newFile, leftNodes); - - Assert.Null(result); - } - - [Fact(DisplayName = "Match_匹配但newFile文件不存在_返回null")] - public void Match_FoundButNewFileNotExist_ReturnsNull() - { - var matcher = new DefaultCleanMatcher(); - var nonExistent = Path.Combine(_testDir, "nonexistent2.dll"); - - var oldFile = CreateFileNode("app.dll", "sub"); - var newFile = new GeneralUpdate.Core.FileSystem.FileNode - { - Name = "app.dll", - RelativePath = "sub", - FullName = nonExistent - }; - var leftNodes = new List { oldFile }; - - var result = matcher.Match(newFile, leftNodes); - - Assert.Null(result); - } - - [Fact(DisplayName = "Match_leftNodes为空_返回null")] - public void Match_EmptyLeftNodes_ReturnsNull() - { - var matcher = new DefaultCleanMatcher(); - var newFile = CreateFileNode("app.dll", "sub"); - var leftNodes = Enumerable.Empty(); - - var result = matcher.Match(newFile, leftNodes); - - Assert.Null(result); - } - - [Fact(DisplayName = "Match_名称大小写不同_仍然匹配(OrdinalIgnoreCase)")] - public void Match_DifferentCaseName_StillMatches() - { - var matcher = new DefaultCleanMatcher(); - - var oldFile = CreateFileNode("APP.DLL", "sub"); - var newFile = CreateFileNode("app.dll", "sub"); - var leftNodes = new List { oldFile }; - - var result = matcher.Match(newFile, leftNodes); - - Assert.NotNull(result); - } - - [Fact(DisplayName = "Match_相对路径大小写不同_仍然匹配")] - public void Match_DifferentCasePath_StillMatches() - { - var matcher = new DefaultCleanMatcher(); - - var oldFile = CreateFileNode("app.dll", "SUB"); - var newFile = CreateFileNode("app.dll", "sub"); - var leftNodes = new List { oldFile }; - - var result = matcher.Match(newFile, leftNodes); - - Assert.NotNull(result); - } - - [Fact(DisplayName = "Match_leftNodes有多个条目_匹配第一个")] - public void Match_MultipleLeftNodes_MatchesFirst() - { - var matcher = new DefaultCleanMatcher(); - - var oldFile1 = CreateFileNode("app.dll", "sub"); - var oldFile2 = CreateFileNode("app_v2.dll", "sub"); - var newFile = CreateFileNode("app.dll", "sub"); - var leftNodes = new List { oldFile1, oldFile2 }; - - var result = matcher.Match(newFile, leftNodes); - - Assert.NotNull(result); - Assert.Equal(oldFile1.FullName, result.FullName); - } - - /// - /// 创建真实存在的测试文件及其FileNode - /// - private GeneralUpdate.Core.FileSystem.FileNode CreateFileNode(string name, string relativeDir) - { - var dir = Path.Combine(_testDir, relativeDir); - Directory.CreateDirectory(dir); - var filePath = Path.Combine(dir, name); - File.WriteAllText(filePath, "test content"); - - return new GeneralUpdate.Core.FileSystem.FileNode - { - Name = name, - RelativePath = relativeDir, - FullName = filePath - }; - } - } -} diff --git a/src/c#/DifferentialTest/Matchers/DefaultDirtyMatcherTests.cs b/src/c#/DifferentialTest/Matchers/DefaultDirtyMatcherTests.cs deleted file mode 100644 index c075e7ca..00000000 --- a/src/c#/DifferentialTest/Matchers/DefaultDirtyMatcherTests.cs +++ /dev/null @@ -1,161 +0,0 @@ -using GeneralUpdate.Core.Differential; - -namespace DifferentialTest.Matchers -{ - /// - /// 分支覆盖点: - /// 1. Match — 通过文件名(去掉.patch后缀后)匹配 (case-insensitive) - /// 2. Match — 文件有双重后缀 (如 app.dll.patch) → 只去掉最后一个.patch - /// 3. Match — 文件名没有.patch后缀 → 直接按oldFile.Name比较 - /// 4. Match — 匹配到但扩展名不是.patch → 返回null - /// 5. Match — patchFiles为空 → null - /// 6. Match — 大小写不敏感匹配 - /// 7. Match — 名字包含.patch但不以.patch结尾 (如 patchfile.txt) - /// - /// 触发条件:各种文件名组合 - /// 预期结果:正确匹配或返回null - /// - public class DefaultDirtyMatcherTests : IDisposable - { - private readonly string _testDir; - - public DefaultDirtyMatcherTests() - { - _testDir = Path.Combine(Path.GetTempPath(), $"DirtyMatcherTests_{Guid.NewGuid():N}"); - Directory.CreateDirectory(_testDir); - } - - public void Dispose() - { - if (Directory.Exists(_testDir)) - Directory.Delete(_testDir, true); - } - - [Fact(DisplayName = "Match_文件名去掉patch后匹配_返回patchFile")] - public void Match_NameWithoutPatchMatches_ReturnsPatchFile() - { - var matcher = new DefaultDirtyMatcher(); - var oldFile = CreateFileInfo("app.dll"); - var patchFile = CreateFileInfo("app.dll.patch"); - var patchFiles = new List { patchFile }; - - var result = matcher.Match(oldFile, patchFiles); - - Assert.NotNull(result); - Assert.Equal(patchFile.FullName, result!.FullName); - } - - [Fact(DisplayName = "Match_双重后缀.patch文件_去掉最后一个patch后缀后匹配")] - public void Match_DoubleExtensionWithPatch_StripsOnlyLastPatch() - { - var matcher = new DefaultDirtyMatcher(); - var oldFile = CreateFileInfo("app.dll.patch"); // 原始文件名含.patch - var patchFile1 = CreateFileInfo("app.dll.patch.patch"); - var patchFiles = new List { patchFile1 }; - - var result = matcher.Match(oldFile, patchFiles); - - Assert.NotNull(result); - Assert.Equal(patchFile1.FullName, result!.FullName); - } - - [Fact(DisplayName = "Match_文件名不匹配_返回null")] - public void Match_NameMismatch_ReturnsNull() - { - var matcher = new DefaultDirtyMatcher(); - var oldFile = CreateFileInfo("app.dll"); - var patchFile = CreateFileInfo("other.dll.patch"); - var patchFiles = new List { patchFile }; - - var result = matcher.Match(oldFile, patchFiles); - - Assert.Null(result); - } - - [Fact(DisplayName = "Match_匹配但扩展名不是.patch_返回null")] - public void Match_MatchedButNotPatchExtension_ReturnsNull() - { - var matcher = new DefaultDirtyMatcher(); - var oldFile = CreateFileInfo("app.dll"); - var patchFile = CreateFileInfo("app.dll.txt"); // 非.patch扩展名 - var patchFiles = new List { patchFile }; - - var result = matcher.Match(oldFile, patchFiles); - - Assert.Null(result); - } - - [Fact(DisplayName = "Match_patchFiles为空_返回null")] - public void Match_EmptyPatchFiles_ReturnsNull() - { - var matcher = new DefaultDirtyMatcher(); - var oldFile = CreateFileInfo("app.dll"); - var patchFiles = Enumerable.Empty(); - - var result = matcher.Match(oldFile, patchFiles); - - Assert.Null(result); - } - - [Fact(DisplayName = "Match_文件名大小写不同_仍然匹配")] - public void Match_DifferentCaseName_StillMatches() - { - var matcher = new DefaultDirtyMatcher(); - var oldFile = CreateFileInfo("APP.DLL"); - var patchFile = CreateFileInfo("app.dll.patch"); - var patchFiles = new List { patchFile }; - - var result = matcher.Match(oldFile, patchFiles); - - Assert.NotNull(result); - } - - [Fact(DisplayName = "Match_patch后缀大小写不同_仍能匹配")] - public void Match_DifferentCasePatchExtension_StillMatches() - { - var matcher = new DefaultDirtyMatcher(); - var oldFile = CreateFileInfo("app.dll"); - var patchFile = CreateFileInfo("app.dll.PATCH"); - var patchFiles = new List { patchFile }; - - var result = matcher.Match(oldFile, patchFiles); - - Assert.NotNull(result); - } - - [Fact(DisplayName = "Match_多个patch文件_匹配第一个")] - public void Match_MultiplePatchFiles_MatchesFirst() - { - var matcher = new DefaultDirtyMatcher(); - var oldFile = CreateFileInfo("app.dll"); - var patchFile1 = CreateFileInfo("app.dll.patch"); - var patchFile2 = CreateFileInfo("app_v2.dll.patch"); - var patchFiles = new List { patchFile1, patchFile2 }; - - var result = matcher.Match(oldFile, patchFiles); - - Assert.NotNull(result); - Assert.Equal(patchFile1.FullName, result!.FullName); - } - - [Fact(DisplayName = "Match_patch文件名没有.patch后缀_直接按名称比较")] - public void Match_NoPatchSuffixInName_ComparesByNameDirectly() - { - var matcher = new DefaultDirtyMatcher(); - var oldFile = CreateFileInfo("readme.txt"); - var patchFile = CreateFileInfo("readme.txt.patch"); - var patchFiles = new List { patchFile }; - - var result = matcher.Match(oldFile, patchFiles); - - Assert.NotNull(result); - } - - private FileInfo CreateFileInfo(string name) - { - var path = Path.Combine(_testDir, name); - File.WriteAllText(path, "test"); - return new FileInfo(path); - } - } -} diff --git a/src/c#/DifferentialTest/Models/DiffProgressTests.cs b/src/c#/DifferentialTest/Models/DiffProgressTests.cs deleted file mode 100644 index 409e0083..00000000 --- a/src/c#/DifferentialTest/Models/DiffProgressTests.cs +++ /dev/null @@ -1,136 +0,0 @@ -using GeneralUpdate.Core.Models; - -namespace DifferentialTest.Models -{ - /// - /// 分支覆盖点: - /// 1. 构造函数正常分支 (completed/total/currentFile/error 不同组合) - /// 2. Percentage — Total == 0 返回 100;Total > 0 返回比率 - /// 3. IsComplete — Completed >= Total (等于/大于) - /// 4. Complete 静态工厂方法 - /// 5. ToString — IsComplete 为 true / false / Error 有值/无值 - /// - /// 触发条件:各属性组合 - /// 预期结果:属性值、百分比、完成标志、字符串格式符合预期 - /// - public class DiffProgressTests - { - [Fact(DisplayName = "构造函数_正常赋值_属性正确返回")] - public void Constructor_ValidValues_PropertiesReturnCorrectly() - { - var progress = new DiffProgress(5, 10, "file.dll"); - - Assert.Equal(5, progress.Completed); - Assert.Equal(10, progress.Total); - Assert.Equal("file.dll", progress.CurrentFile); - Assert.Null(progress.Error); - } - - [Fact(DisplayName = "构造函数_带Error参数_Error属性正确返回")] - public void Constructor_WithError_ErrorPropertyReturnsCorrectly() - { - var progress = new DiffProgress(3, 10, "bad.dll", "IO error"); - - Assert.Equal("IO error", progress.Error); - } - - [Theory(DisplayName = "Percentage_不同Total值_返回正确百分比")] - [InlineData(5, 10, 50.0)] - [InlineData(0, 10, 0.0)] - [InlineData(10, 10, 100.0)] - [InlineData(1, 3, 100.0 / 3.0)] - [InlineData(0, 0, 100.0)] - [InlineData(7, 1, 700.0)] - public void Percentage_VariousTotals_ReturnsCorrectPercentage(int completed, int total, double expected) - { - var progress = new DiffProgress(completed, total, null); - - Assert.Equal(expected, progress.Percentage); - } - - [Theory(DisplayName = "IsComplete_Completed与Total的关系_正确返回完成标志")] - [InlineData(5, 10, false)] - [InlineData(10, 10, true)] - [InlineData(0, 0, true)] - [InlineData(11, 10, true)] - public void IsComplete_CompletedVsTotal_ReturnsCorrectFlag(int completed, int total, bool expected) - { - var progress = new DiffProgress(completed, total, null); - - Assert.Equal(expected, progress.IsComplete); - } - - [Fact(DisplayName = "Complete_静态工厂_返回已完成标记")] - public void Complete_StaticFactory_ReturnsCompleteMarker() - { - var progress = DiffProgress.Complete(42); - - Assert.Equal(42, progress.Completed); - Assert.Equal(42, progress.Total); - Assert.Null(progress.CurrentFile); - Assert.Null(progress.Error); - Assert.True(progress.IsComplete); - } - - [Fact(DisplayName = "ToString_已完成状态_返回格式化的完成字符串")] - public void ToString_CompleteState_ReturnsFormattedCompleteString() - { - var progress = DiffProgress.Complete(10); - - var result = progress.ToString(); - - Assert.Contains("Complete", result); - Assert.Contains("10/10", result); - } - - [Fact(DisplayName = "ToString_进行中状态_返回百分比和处理文件")] - public void ToString_InProgressState_ReturnsPercentageAndFile() - { - var progress = new DiffProgress(5, 10, "app.exe"); - - var result = progress.ToString(); - - Assert.Contains("5/10", result); - Assert.Contains("app.exe", result); - Assert.DoesNotContain("failed", result); - } - - [Fact(DisplayName = "ToString_带Error的进行中状态_包含失败信息")] - public void ToString_InProgressWithError_ContainsFailureInfo() - { - var progress = new DiffProgress(3, 10, "bad.dll", "access denied"); - - var result = progress.ToString(); - - Assert.Contains("failed: access denied", result); - } - - [Fact(DisplayName = "ToString_CurrentFile为null_显示省略号")] - public void ToString_NullCurrentFile_ShowsEllipsis() - { - var progress = new DiffProgress(1, 10, null); - - var result = progress.ToString(); - - Assert.Contains("...", result); - } - - [Fact(DisplayName = "DiffProgress_值类型语义_相等比较")] - public void ValueTypeSemantics_EqualityComparison() - { - var a = new DiffProgress(1, 10, "f"); - var b = new DiffProgress(1, 10, "f"); - - Assert.True(a.Equals(b)); - Assert.Equal(a.GetHashCode(), b.GetHashCode()); - } - - [Fact(DisplayName = "DiffProgress_CurrentFile为null_属性返回null")] - public void NullCurrentFile_ReturnsNull() - { - var progress = new DiffProgress(0, 10, null); - - Assert.Null(progress.CurrentFile); - } - } -} diff --git a/src/c#/DifferentialTest/Pipeline/DiffPipelineBuilderTests.cs b/src/c#/DifferentialTest/Pipeline/DiffPipelineBuilderTests.cs deleted file mode 100644 index 3862508d..00000000 --- a/src/c#/DifferentialTest/Pipeline/DiffPipelineBuilderTests.cs +++ /dev/null @@ -1,195 +0,0 @@ -using Moq; -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Core.Differential; -using GeneralUpdate.Core.Models; -using GeneralUpdate.Core.Pipeline; - -namespace DifferentialTest.Pipeline -{ - /// - /// 分支覆盖点: - /// 1. UseDiffer — differ为null → ArgumentNullException - /// 2. UseDiffer — 正常设置 - /// 3. UseCleanMatcher — matcher为null → ArgumentNullException - /// 4. UseCleanMatcher — 正常设置 - /// 5. UseDirtyMatcher — matcher为null → ArgumentNullException - /// 6. UseDirtyMatcher — 正常设置 - /// 7. WithParallelism — maxDegreeOfParallelism < 1 → ArgumentOutOfRangeException - /// 8. WithParallelism — 正常/边界值 - /// 9. WithStopOnFirstError — true/false - /// 10. WithProgress — progress为null → ArgumentNullException - /// 11. WithProgress — 正常设置 - /// 12. Build — 未设置任何选项 → 全部默认值 - /// 13. Build — 全部自定义 → 传给DiffPipeline - /// 14. Build — differ未设置 → 默认StreamingHdiffDiffer - /// - /// 触发条件:各种构造组合 - /// 预期结果:正确构建DiffPipeline - /// - public class DiffPipelineBuilderTests - { - [Fact(DisplayName = "Build_未设置任何选项_使用所有默认值")] - public void Build_NoOptions_UsesAllDefaults() - { - var pipeline = new DiffPipelineBuilder().Build(); - - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "UseDiffer_有效differ_正确设置")] - public void UseDiffer_ValidDiffer_SetsCorrectly() - { - var mockDiffer = new Mock(); - var builder = new DiffPipelineBuilder(); - - var result = builder.UseDiffer(mockDiffer.Object); - var pipeline = result.Build(); - - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "UseDiffer_null_抛出ArgumentNullException")] - public void UseDiffer_Null_ThrowsArgumentNullException() - { - var builder = new DiffPipelineBuilder(); - - var ex = Assert.Throws(() => builder.UseDiffer(null!)); - - Assert.Equal("differ", ex.ParamName); - } - - [Fact(DisplayName = "UseCleanMatcher_有效matcher_正确设置")] - public void UseCleanMatcher_ValidMatcher_SetsCorrectly() - { - var mockMatcher = new Mock(); - - var pipeline = new DiffPipelineBuilder() - .UseCleanMatcher(mockMatcher.Object) - .Build(); - - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "UseCleanMatcher_null_抛出ArgumentNullException")] - public void UseCleanMatcher_Null_ThrowsArgumentNullException() - { - var builder = new DiffPipelineBuilder(); - - var ex = Assert.Throws(() => builder.UseCleanMatcher(null!)); - - Assert.Equal("matcher", ex.ParamName); - } - - [Fact(DisplayName = "UseDirtyMatcher_有效matcher_正确设置")] - public void UseDirtyMatcher_ValidMatcher_SetsCorrectly() - { - var mockMatcher = new Mock(); - - var pipeline = new DiffPipelineBuilder() - .UseDirtyMatcher(mockMatcher.Object) - .Build(); - - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "UseDirtyMatcher_null_抛出ArgumentNullException")] - public void UseDirtyMatcher_Null_ThrowsArgumentNullException() - { - var builder = new DiffPipelineBuilder(); - - var ex = Assert.Throws(() => builder.UseDirtyMatcher(null!)); - - Assert.Equal("matcher", ex.ParamName); - } - - [Theory(DisplayName = "WithParallelism_有效值_正确设置")] - [InlineData(1)] - [InlineData(4)] - [InlineData(16)] - [InlineData(32)] - public void WithParallelism_ValidValues_SetsCorrectly(int parallelism) - { - var pipeline = new DiffPipelineBuilder() - .WithParallelism(parallelism) - .Build(); - - Assert.NotNull(pipeline); - } - - [Theory(DisplayName = "WithParallelism_无效值_抛出ArgumentOutOfRangeException")] - [InlineData(0)] - [InlineData(-1)] - [InlineData(int.MinValue)] - public void WithParallelism_InvalidValues_ThrowsArgumentOutOfRangeException(int invalidValue) - { - var builder = new DiffPipelineBuilder(); - - Assert.Throws(() => builder.WithParallelism(invalidValue)); - } - - [Theory(DisplayName = "WithStopOnFirstError_不同值_正确设置")] - [InlineData(true)] - [InlineData(false)] - public void WithStopOnFirstError_VariousValues_SetsCorrectly(bool stopOnFirstError) - { - var pipeline = new DiffPipelineBuilder() - .WithStopOnFirstError(stopOnFirstError) - .Build(); - - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "WithProgress_有效progress_正确设置")] - public void WithProgress_ValidProgress_SetsCorrectly() - { - var mockProgress = new Mock>(); - - var pipeline = new DiffPipelineBuilder() - .WithProgress(mockProgress.Object) - .Build(); - - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "WithProgress_null_抛出ArgumentNullException")] - public void WithProgress_Null_ThrowsArgumentNullException() - { - var builder = new DiffPipelineBuilder(); - - var ex = Assert.Throws(() => builder.WithProgress(null!)); - - Assert.Equal("progress", ex.ParamName); - } - - [Fact(DisplayName = "链式调用_全部自定义_构建成功")] - public void FluentApi_AllCustom_BuildsSuccessfully() - { - var mockDiffer = new Mock(); - var mockCleanMatcher = new Mock(); - var mockDirtyMatcher = new Mock(); - var mockProgress = new Mock>(); - - var pipeline = new DiffPipelineBuilder() - .UseDiffer(mockDiffer.Object) - .UseCleanMatcher(mockCleanMatcher.Object) - .UseDirtyMatcher(mockDirtyMatcher.Object) - .WithParallelism(4) - .WithStopOnFirstError(true) - .WithProgress(mockProgress.Object) - .Build(); - - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "Build_重复调用_每次返回新实例")] - public void Build_MultipleCalls_ReturnsDifferentInstances() - { - var builder = new DiffPipelineBuilder().WithParallelism(2); - - var p1 = builder.Build(); - var p2 = builder.Build(); - - Assert.NotSame(p1, p2); - } - } -} diff --git a/src/c#/DifferentialTest/Pipeline/DiffPipelineIntegrationTests.cs b/src/c#/DifferentialTest/Pipeline/DiffPipelineIntegrationTests.cs deleted file mode 100644 index 03f8ee86..00000000 --- a/src/c#/DifferentialTest/Pipeline/DiffPipelineIntegrationTests.cs +++ /dev/null @@ -1,621 +0,0 @@ -using System.IO.Compression; -using Moq; -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Core.Differential; -using GeneralUpdate.Core.Models; -using GeneralUpdate.Core.Pipeline; - -namespace DifferentialTest.Pipeline -{ - /// - /// DiffPipeline 集成测试:端到端验证 Clean/Dirty 管线的文件级 patch 生成与应用。 - /// 每个测试创建临时目录,在 Dispose 中清理。 - /// - public class DiffPipelineIntegrationTests : IDisposable - { - private readonly string _testDir; - - public DiffPipelineIntegrationTests() - { - _testDir = Path.Combine(Path.GetTempPath(), $"DPI_IT_{Guid.NewGuid():N}"); - Directory.CreateDirectory(_testDir); - } - - public void Dispose() - { - if (Directory.Exists(_testDir)) - { - try { Directory.Delete(_testDir, true); } catch { } - } - } - - // ---- helpers ---- - - private string GetPath(string relative) => Path.Combine(_testDir, relative); - - private static void CopyDirectory(string sourceDir, string destDir) - { - Directory.CreateDirectory(destDir); - foreach (var file in Directory.GetFiles(sourceDir)) - File.Copy(file, Path.Combine(destDir, Path.GetFileName(file)), true); - foreach (var dir in Directory.GetDirectories(sourceDir)) - CopyDirectory(dir, Path.Combine(destDir, Path.GetFileName(dir))); - } - - private static (IProgress Progress, List Captured) CreateProgressCapture() - { - var captured = new List(); - var progress = new Progress(p => captured.Add(p)); - return (progress, captured); - } - - // ================================================================ - // CleanAsync 集成测试 - // ================================================================ - - [Fact(DisplayName = "CleanAsync_单个修改文件_生成补丁文件")] - public async Task CleanAsync_SingleModifiedFile_GeneratesPatch() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - - var oldData = new byte[200]; - new Random(1).NextBytes(oldData); - var newData = new byte[200]; - oldData.CopyTo(newData, 0); - newData[50] ^= 0xFF; // modify one byte - - File.WriteAllBytes(Path.Combine(sourceDir, "file.bin"), oldData); - File.WriteAllBytes(Path.Combine(targetDir, "file.bin"), newData); - - var pipeline = new DiffPipeline(new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }); - - // Act - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - // Assert - var patchFile = Path.Combine(patchDir, "file.bin.patch"); - Assert.True(File.Exists(patchFile)); - Assert.True(new FileInfo(patchFile).Length > 0); - } - - [Fact(DisplayName = "CleanAsync_多文件_生成所有补丁且不处理无变更文件")] - public async Task CleanAsync_MultipleFiles_GeneratesAllPatches() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - - var rng = new Random(42); - var shared = new byte[100]; - rng.NextBytes(shared); - - // 3 modified files - byte[] mod1Old = new byte[100]; shared.CopyTo(mod1Old, 0); - byte[] mod1New = new byte[100]; shared.CopyTo(mod1New, 0); mod1New[30] = 88; - - var mod2Old = new byte[200]; rng.NextBytes(mod2Old); - var mod2New = new byte[200]; rng.NextBytes(mod2New); - - var mod3Old = new byte[50]; rng.NextBytes(mod3Old); - var mod3New = new byte[50]; rng.NextBytes(mod3New); - - // 1 identical file - var ident = new byte[150]; rng.NextBytes(ident); - - // 1 new file (only in target) - var newFileData = new byte[80]; rng.NextBytes(newFileData); - - File.WriteAllBytes(Path.Combine(sourceDir, "mod1.bin"), mod1Old); - File.WriteAllBytes(Path.Combine(sourceDir, "mod2.bin"), mod2Old); - File.WriteAllBytes(Path.Combine(sourceDir, "mod3.bin"), mod3Old); - File.WriteAllBytes(Path.Combine(sourceDir, "ident.bin"), ident); - // "new1.bin" intentionally NOT written to source - - File.WriteAllBytes(Path.Combine(targetDir, "mod1.bin"), mod1New); - File.WriteAllBytes(Path.Combine(targetDir, "mod2.bin"), mod2New); - File.WriteAllBytes(Path.Combine(targetDir, "mod3.bin"), mod3New); - File.WriteAllBytes(Path.Combine(targetDir, "ident.bin"), ident); - File.WriteAllBytes(Path.Combine(targetDir, "new1.bin"), newFileData); - - var pipeline = new DiffPipeline(new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }); - - // Act - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - // Assert - // 3 modified → .patch files - Assert.True(File.Exists(Path.Combine(patchDir, "mod1.bin.patch"))); - Assert.True(File.Exists(Path.Combine(patchDir, "mod2.bin.patch"))); - Assert.True(File.Exists(Path.Combine(patchDir, "mod3.bin.patch"))); - // 1 new → copied directly (not patched) - Assert.True(File.Exists(Path.Combine(patchDir, "new1.bin"))); - // 1 identical → no output - Assert.False(File.Exists(Path.Combine(patchDir, "ident.bin.patch"))); - Assert.False(File.Exists(Path.Combine(patchDir, "ident.bin"))); - } - - [Fact(DisplayName = "CleanAsync_目标有新文件_直接复制而非打补丁")] - public async Task CleanAsync_NewFile_CopiedDirectly() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - - var data = new byte[120]; - new Random(7).NextBytes(data); - - // source has NO newfile.bin - File.WriteAllBytes(Path.Combine(targetDir, "newfile.bin"), data); - - var pipeline = new DiffPipeline(new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }); - - // Act - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - // Assert: copied, not patched - var copied = Path.Combine(patchDir, "newfile.bin"); - var patched = Path.Combine(patchDir, "newfile.bin.patch"); - Assert.True(File.Exists(copied)); - Assert.False(File.Exists(patched)); - Assert.Equal(data, File.ReadAllBytes(copied)); - } - - [Fact(DisplayName = "CleanAsync_相同文件_不生成补丁也不复制")] - public async Task CleanAsync_IdenticalFile_NoPatch() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - - var data = new byte[80]; - new Random(5).NextBytes(data); - File.WriteAllBytes(Path.Combine(sourceDir, "ident.bin"), data); - File.WriteAllBytes(Path.Combine(targetDir, "ident.bin"), data); - - var pipeline = new DiffPipeline(new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }); - - // Act - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - // Assert: no .patch and no copy for unchanged file - Assert.False(File.Exists(Path.Combine(patchDir, "ident.bin.patch"))); - Assert.False(File.Exists(Path.Combine(patchDir, "ident.bin"))); - } - - [Fact(DisplayName = "CleanAsync_源有目标无_写入删除列表JSON")] - public async Task CleanAsync_DeletedFile_WritesDeleteJson() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - - var data = new byte[60]; - new Random(3).NextBytes(data); - File.WriteAllBytes(Path.Combine(sourceDir, "delete_me.bin"), data); - // target is intentionally empty (no delete_me.bin) - - var pipeline = new DiffPipeline(new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }); - - // Act - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - // Assert - var deleteJson = Path.Combine(patchDir, "generalupdate.delete.json"); - Assert.True(File.Exists(deleteJson)); - var content = File.ReadAllText(deleteJson); - Assert.NotEmpty(content); - } - - [Fact(DisplayName = "CleanAsync_进度回调_最终Completed等于Total")] - public async Task CleanAsync_WithProgress_ReportsProgress() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - - var rng = new Random(12); - for (int i = 1; i <= 5; i++) - { - var oldData = new byte[100]; rng.NextBytes(oldData); - var newData = new byte[100]; rng.NextBytes(newData); - File.WriteAllBytes(Path.Combine(sourceDir, $"f{i}.bin"), oldData); - File.WriteAllBytes(Path.Combine(targetDir, $"f{i}.bin"), newData); - } - - var (progress, captured) = CreateProgressCapture(); - var pipeline = new DiffPipeline( - new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }, - new GeneralUpdate.Differential.Differ.StreamingHdiffDiffer(), - progress: progress); - - // Act - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - // Assert - Assert.NotEmpty(captured); - var last = captured[^1]; - Assert.True(last.IsComplete); - Assert.Equal(5, last.Completed); - Assert.Equal(5, last.Total); - } - - [Fact(DisplayName = "CleanAsync_已取消令牌_抛出OperationCanceledException")] - public async Task CleanAsync_Cancellation_ThrowsOperationCanceledException() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - - File.WriteAllBytes(Path.Combine(sourceDir, "a.bin"), new byte[] { 1, 2, 3 }); - File.WriteAllBytes(Path.Combine(targetDir, "a.bin"), new byte[] { 4, 5, 6 }); - - var pipeline = new DiffPipeline(); - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - // Act & Assert - await Assert.ThrowsAsync(() => - pipeline.CleanAsync(sourceDir, targetDir, patchDir, cancellationToken: cts.Token)); - } - - [Fact(DisplayName = "CleanAsync_StopOnFirstError为false_一个文件出错其他继续")] - public async Task CleanAsync_StopOnFirstError_False_ContinuesOnError() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - - var rng = new Random(99); - var aOld = new byte[100]; rng.NextBytes(aOld); - var aNew = new byte[100]; rng.NextBytes(aNew); - var bOld = new byte[80]; rng.NextBytes(bOld); - var bNew = new byte[80]; rng.NextBytes(bNew); - - File.WriteAllBytes(Path.Combine(sourceDir, "a.bin"), aOld); - File.WriteAllBytes(Path.Combine(sourceDir, "b.bin"), bOld); - File.WriteAllBytes(Path.Combine(targetDir, "a.bin"), aNew); - File.WriteAllBytes(Path.Combine(targetDir, "b.bin"), bNew); - - // Mock differ: a.bin fails, b.bin succeeds - var mockDiffer = new Mock(); - mockDiffer - .Setup(d => d.CleanAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns((string old, string _, string patch, CancellationToken ct) => - { - if (Path.GetFileName(old) == "a.bin") - return Task.FromException(new IOException("Simulated error for a.bin")); - - if (!File.Exists(patch)) - File.WriteAllBytes(patch, new byte[10]); - return Task.CompletedTask; - }); - - var (progress, captured) = CreateProgressCapture(); - var options = new DiffPipelineOptions { MaxDegreeOfParallelism = 1, StopOnFirstError = false }; - var pipeline = new DiffPipeline(options, mockDiffer.Object, progress: progress); - - // Act - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - // Assert - // a.bin → error, no patch - Assert.False(File.Exists(Path.Combine(patchDir, "a.bin.patch"))); - // b.bin → succeeded, patch exists - Assert.True(File.Exists(Path.Combine(patchDir, "b.bin.patch"))); - - // progress should include an error entry and complete - var errors = captured.Where(p => p.Error != null).ToList(); - Assert.NotEmpty(errors); - } - - // ================================================================ - // DirtyAsync 集成测试 - // ================================================================ - - [Fact(DisplayName = "DirtyAsync_应用多个补丁_正确更新文件")] - public async Task DirtyAsync_AppliesMultiplePatches_Correctly() - { - // Arrange — Phase 1: generate patches via Clean - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - var appDir = GetPath("app"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - Directory.CreateDirectory(appDir); - - var rng = new Random(42); - var aOld = new byte[200]; rng.NextBytes(aOld); - var aNew = new byte[200]; rng.NextBytes(aNew); - var bOld = new byte[300]; rng.NextBytes(bOld); - var bNew = new byte[300]; rng.NextBytes(bNew); - - File.WriteAllBytes(Path.Combine(sourceDir, "a.bin"), aOld); - File.WriteAllBytes(Path.Combine(sourceDir, "b.bin"), bOld); - File.WriteAllBytes(Path.Combine(targetDir, "a.bin"), aNew); - File.WriteAllBytes(Path.Combine(targetDir, "b.bin"), bNew); - - var pipeline = new DiffPipeline(new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }); - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - // Verify patches were generated - Assert.True(File.Exists(Path.Combine(patchDir, "a.bin.patch"))); - Assert.True(File.Exists(Path.Combine(patchDir, "b.bin.patch"))); - - // Phase 2: set up app dir with old files, patch dir copy - File.WriteAllBytes(Path.Combine(appDir, "a.bin"), aOld); - File.WriteAllBytes(Path.Combine(appDir, "b.bin"), bOld); - - var patch2Dir = GetPath("patch2"); - CopyDirectory(patchDir, patch2Dir); - - // Act - await pipeline.DirtyAsync(appDir, patch2Dir); - - // Assert: app files now match target versions - Assert.Equal(aNew, File.ReadAllBytes(Path.Combine(appDir, "a.bin"))); - Assert.Equal(bNew, File.ReadAllBytes(Path.Combine(appDir, "b.bin"))); - } - - [Fact(DisplayName = "DirtyAsync_有删除列表_删除标记文件")] - public async Task DirtyAsync_WithDeleteList_DeletesMarkedFiles() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - var appDir = GetPath("app"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - Directory.CreateDirectory(appDir); - - var keepData = new byte[60]; - var deleteData = new byte[80]; - new Random(1).NextBytes(keepData); - new Random(2).NextBytes(deleteData); - - // Source has delete_me.bin, target does NOT - File.WriteAllBytes(Path.Combine(sourceDir, "keep.bin"), keepData); - File.WriteAllBytes(Path.Combine(sourceDir, "delete_me.bin"), deleteData); - File.WriteAllBytes(Path.Combine(targetDir, "keep.bin"), keepData); - - var pipeline = new DiffPipeline(new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }); - - // Phase 1: Clean generates delete JSON - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - Assert.True(File.Exists(Path.Combine(patchDir, "generalupdate.delete.json"))); - - // Phase 2: app dir has both files - File.WriteAllBytes(Path.Combine(appDir, "keep.bin"), keepData); - File.WriteAllBytes(Path.Combine(appDir, "delete_me.bin"), deleteData); - - var patch2Dir = GetPath("patch2"); - CopyDirectory(patchDir, patch2Dir); - - // Act - await pipeline.DirtyAsync(appDir, patch2Dir); - - // Assert: keep.bin stays, delete_me.bin is deleted - Assert.True(File.Exists(Path.Combine(appDir, "keep.bin"))); - Assert.False(File.Exists(Path.Combine(appDir, "delete_me.bin"))); - } - - [Fact(DisplayName = "DirtyAsync_补丁目录有额外未知文件_复制到应用目录")] - public async Task DirtyAsync_UnknownFiles_CopiedToAppDir() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - var appDir = GetPath("app"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - Directory.CreateDirectory(appDir); - - var data = new byte[50]; - new Random(3).NextBytes(data); - File.WriteAllBytes(Path.Combine(sourceDir, "a.bin"), data); - File.WriteAllBytes(Path.Combine(targetDir, "a.bin"), data); - - // Phase 1: Clean (no differences → empty patch dir) - var pipeline = new DiffPipeline(new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }); - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - // Add an "unknown" file (not .patch) to patch dir - var unknownData = new byte[] { 100, 101, 102, 103 }; - File.WriteAllBytes(Path.Combine(patchDir, "config.xml"), unknownData); - - // App dir has original (identical) file - File.WriteAllBytes(Path.Combine(appDir, "a.bin"), data); - - // Act - await pipeline.DirtyAsync(appDir, patchDir); - - // Assert: unknown file was copied to app dir - var copied = Path.Combine(appDir, "config.xml"); - Assert.True(File.Exists(copied)); - Assert.Equal(unknownData, File.ReadAllBytes(copied)); - } - - [Fact(DisplayName = "DirtyAsync_进度回调_最终Completed等于Total")] - public async Task DirtyAsync_WithProgress_ReportsProgress() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - var appDir = GetPath("app"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - Directory.CreateDirectory(appDir); - - var rng = new Random(55); - for (int i = 1; i <= 3; i++) - { - var oldData = new byte[100]; rng.NextBytes(oldData); - var newData = new byte[100]; rng.NextBytes(newData); - File.WriteAllBytes(Path.Combine(sourceDir, $"f{i}.bin"), oldData); - File.WriteAllBytes(Path.Combine(targetDir, $"f{i}.bin"), newData); - File.WriteAllBytes(Path.Combine(appDir, $"f{i}.bin"), oldData); - } - - var pipeline = new DiffPipeline(new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }); - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - var patch2Dir = GetPath("patch2"); - CopyDirectory(patchDir, patch2Dir); - - var (progress, captured) = CreateProgressCapture(); - - // Act - await pipeline.DirtyAsync(appDir, patch2Dir, progress: progress); - - // Assert - Assert.NotEmpty(captured); - var last = captured[^1]; - Assert.True(last.IsComplete); - Assert.True(last.Completed >= 3); - } - - [Fact(DisplayName = "DirtyAsync_已取消令牌_抛出OperationCanceledException")] - public async Task DirtyAsync_Cancellation_ThrowsOperationCanceledException() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - var appDir = GetPath("app"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - Directory.CreateDirectory(appDir); - - var data = new byte[50]; new Random(1).NextBytes(data); - var newData = new byte[50]; new Random(2).NextBytes(newData); - File.WriteAllBytes(Path.Combine(sourceDir, "a.bin"), data); - File.WriteAllBytes(Path.Combine(targetDir, "a.bin"), newData); - File.WriteAllBytes(Path.Combine(appDir, "a.bin"), data); - - var pipeline = new DiffPipeline(new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }); - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - var patch2Dir = GetPath("patch2"); - CopyDirectory(patchDir, patch2Dir); - - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - // Act & Assert - await Assert.ThrowsAsync(() => - pipeline.DirtyAsync(appDir, patch2Dir, cancellationToken: cts.Token)); - } - - [Fact(DisplayName = "端到端往返_多个文件Clean再Dirty_输出与目标一致")] - public async Task FullPipeline_RoundTrip_ProducesIdenticalOutput() - { - // Arrange - var sourceDir = GetPath("source"); - var targetDir = GetPath("target"); - var patchDir = GetPath("patch"); - var appDir = GetPath("app"); - Directory.CreateDirectory(sourceDir); - Directory.CreateDirectory(targetDir); - Directory.CreateDirectory(patchDir); - Directory.CreateDirectory(appDir); - - var rng = new Random(1234); - - // Create 3 modified + 1 new + 1 identical - var aOld = new byte[200]; rng.NextBytes(aOld); - var aNew = new byte[200]; rng.NextBytes(aNew); - - var bOld = new byte[300]; rng.NextBytes(bOld); - var bNew = new byte[300]; rng.NextBytes(bNew); - - var cOld = new byte[150]; rng.NextBytes(cOld); - var cNew = new byte[150]; rng.NextBytes(cNew); - - var ident = new byte[100]; rng.NextBytes(ident); - var onlyNew = new byte[80]; rng.NextBytes(onlyNew); - - // Source (old version) - File.WriteAllBytes(Path.Combine(sourceDir, "a.bin"), aOld); - File.WriteAllBytes(Path.Combine(sourceDir, "b.bin"), bOld); - File.WriteAllBytes(Path.Combine(sourceDir, "c.bin"), cOld); - File.WriteAllBytes(Path.Combine(sourceDir, "ident.bin"), ident); - - // Target (new version) - File.WriteAllBytes(Path.Combine(targetDir, "a.bin"), aNew); - File.WriteAllBytes(Path.Combine(targetDir, "b.bin"), bNew); - File.WriteAllBytes(Path.Combine(targetDir, "c.bin"), cNew); - File.WriteAllBytes(Path.Combine(targetDir, "ident.bin"), ident); - File.WriteAllBytes(Path.Combine(targetDir, "newfile.bin"), onlyNew); - - var pipeline = new DiffPipeline(new DiffPipelineOptions { MaxDegreeOfParallelism = 1 }); - - // Phase 1: Clean - await pipeline.CleanAsync(sourceDir, targetDir, patchDir); - - // Phase 2: set up app dir as old version - File.WriteAllBytes(Path.Combine(appDir, "a.bin"), aOld); - File.WriteAllBytes(Path.Combine(appDir, "b.bin"), bOld); - File.WriteAllBytes(Path.Combine(appDir, "c.bin"), cOld); - File.WriteAllBytes(Path.Combine(appDir, "ident.bin"), ident); - - var patch2Dir = GetPath("patch2"); - CopyDirectory(patchDir, patch2Dir); - - // Phase 3: Dirty - await pipeline.DirtyAsync(appDir, patch2Dir); - - // Assert: all target files present with correct content - Assert.Equal(aNew, File.ReadAllBytes(Path.Combine(appDir, "a.bin"))); - Assert.Equal(bNew, File.ReadAllBytes(Path.Combine(appDir, "b.bin"))); - Assert.Equal(cNew, File.ReadAllBytes(Path.Combine(appDir, "c.bin"))); - Assert.Equal(ident, File.ReadAllBytes(Path.Combine(appDir, "ident.bin"))); - Assert.Equal(onlyNew, File.ReadAllBytes(Path.Combine(appDir, "newfile.bin"))); - } - } -} diff --git a/src/c#/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs b/src/c#/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs deleted file mode 100644 index 4ab380f8..00000000 --- a/src/c#/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -using GeneralUpdate.Core.Pipeline; - -namespace DifferentialTest.Pipeline -{ - /// - /// 分支覆盖点: - /// 1. MaxDegreeOfParallelism — 默认 = 2 - /// 2. StopOnFirstError — 默认 = false - /// 3. DeletePatchAfterApply — 默认 = true - /// 4. 属性 set/get — 修改后正确返回 - /// 5. 边界值 — MaxDegreeOfParallelism=0 (合法,由调用者检查) - /// - /// 触发条件:属性get/set操作 - /// 预期结果:默认值符合规范,set后get返回新值 - /// - public class DiffPipelineOptionsTests - { - [Fact(DisplayName = "默认构造_MaxDegreeOfParallelism为2")] - public void DefaultConstructor_MaxDegreeOfParallelism_Equals2() - { - var options = new DiffPipelineOptions(); - - Assert.Equal(2, options.MaxDegreeOfParallelism); - } - - [Fact(DisplayName = "默认构造_StopOnFirstError为false")] - public void DefaultConstructor_StopOnFirstError_IsFalse() - { - var options = new DiffPipelineOptions(); - - Assert.False(options.StopOnFirstError); - } - - [Fact(DisplayName = "默认构造_DeletePatchAfterApply为true")] - public void DefaultConstructor_DeletePatchAfterApply_IsTrue() - { - var options = new DiffPipelineOptions(); - - Assert.True(options.DeletePatchAfterApply); - } - - [Fact(DisplayName = "MaxDegreeOfParallelism_set_设置后get返回新值")] - public void MaxDegreeOfParallelism_Set_ReturnsNewValue() - { - var options = new DiffPipelineOptions { MaxDegreeOfParallelism = 8 }; - - Assert.Equal(8, options.MaxDegreeOfParallelism); - } - - [Fact(DisplayName = "StopOnFirstError_set_设置后get返回新值")] - public void StopOnFirstError_Set_ReturnsNewValue() - { - var options = new DiffPipelineOptions { StopOnFirstError = true }; - - Assert.True(options.StopOnFirstError); - } - - [Fact(DisplayName = "DeletePatchAfterApply_set_设置后get返回新值")] - public void DeletePatchAfterApply_Set_ReturnsNewValue() - { - var options = new DiffPipelineOptions { DeletePatchAfterApply = false }; - - Assert.False(options.DeletePatchAfterApply); - } - - [Theory(DisplayName = "MaxDegreeOfParallelism_边界值_正确设置")] - [InlineData(0)] - [InlineData(1)] - [InlineData(64)] - [InlineData(int.MaxValue)] - public void MaxDegreeOfParallelism_BoundaryValues_SetsCorrectly(int value) - { - var options = new DiffPipelineOptions { MaxDegreeOfParallelism = value }; - - Assert.Equal(value, options.MaxDegreeOfParallelism); - } - } -} diff --git a/src/c#/DifferentialTest/Pipeline/DiffPipelineTests.cs b/src/c#/DifferentialTest/Pipeline/DiffPipelineTests.cs deleted file mode 100644 index 4dce4b05..00000000 --- a/src/c#/DifferentialTest/Pipeline/DiffPipelineTests.cs +++ /dev/null @@ -1,177 +0,0 @@ -using Moq; -using GeneralUpdate.Differential.Abstractions; -using GeneralUpdate.Core.Differential; -using GeneralUpdate.Core.Models; -using GeneralUpdate.Core.Pipeline; - -namespace DifferentialTest.Pipeline -{ - /// - /// 分支覆盖点:构造函数、参数验证、异常分支。 - /// 触发条件:各种构造参数和运行时参数的组合。 - /// 预期结果:参数验证、异常分支正确。 - /// - public class DiffPipelineTests - { - [Fact(DisplayName = "构造函数_无参_使用所有默认值")] - public void Constructor_NoArgs_UsesAllDefaults() - { - var pipeline = new DiffPipeline(); - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "构造函数_仅options_默认differ和matchers")] - public void Constructor_OptionsOnly_UsesDefaults() - { - var options = new DiffPipelineOptions { MaxDegreeOfParallelism = 2 }; - var pipeline = new DiffPipeline(options); - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "构造函数_options为null_抛出ArgumentNullException")] - public void Constructor_NullOptions_ThrowsArgumentNullException() - { - var mockDiffer = new Mock(); - var ex = Assert.Throws(() => - { - DiffPipelineOptions? opt = null; - _ = new DiffPipeline(options: opt!, binaryDiffer: mockDiffer.Object, cleanMatcher: (ICleanMatcher?)null); - }); - Assert.Equal("options", ex.ParamName); - } - - [Fact(DisplayName = "构造函数_binaryDiffer为null_抛出ArgumentNullException")] - public void Constructor_NullDiffer_ThrowsArgumentNullException() - { - var options = new DiffPipelineOptions(); - var ex = Assert.Throws(() => - { - IBinaryDiffer? diff = null; - _ = new DiffPipeline(options: options, binaryDiffer: diff!, cleanMatcher: (ICleanMatcher?)null); - }); - Assert.Equal("binaryDiffer", ex.ParamName); - } - - [Fact(DisplayName = "构造函数_cleanMatcher为null_使用DefaultCleanMatcher")] - public void Constructor_NullCleanMatcher_UsesDefault() - { - var options = new DiffPipelineOptions(); - var mockDiffer = new Mock(); - ICleanMatcher? cm = null; - IDirtyMatcher? dm = null; - IProgress? pr = null; - var pipeline = new DiffPipeline(options, mockDiffer.Object, cm, dm, pr); - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "构造函数_dirtyMatcher为null_使用DefaultDirtyMatcher")] - public void Constructor_NullDirtyMatcher_UsesDefault() - { - var options = new DiffPipelineOptions(); - var mockDiffer = new Mock(); - ICleanMatcher? cm = null; - IDirtyMatcher? dm = null; - IProgress? pr = null; - var pipeline = new DiffPipeline(options, mockDiffer.Object, cm, dm, pr); - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "构造函数_progress为null_允许")] - public void Constructor_NullProgress_Allowed() - { - var options = new DiffPipelineOptions(); - var mockDiffer = new Mock(); - ICleanMatcher? cm = null; - IDirtyMatcher? dm = null; - IProgress? pr = null; - var pipeline = new DiffPipeline(options, mockDiffer.Object, cm, dm, pr); - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "构造函数_全参自定义_创建成功")] - public void Constructor_AllCustom_CreatesSuccessfully() - { - var options = new DiffPipelineOptions { MaxDegreeOfParallelism = 4 }; - var mockDiffer = new Mock(); - var mockCleanMatcher = new Mock(); - var mockDirtyMatcher = new Mock(); - var mockProgress = new Mock>(); - var pipeline = new DiffPipeline(options, mockDiffer.Object, - mockCleanMatcher.Object, mockDirtyMatcher.Object, mockProgress.Object); - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "构造函数_兼容3参重载_创建成功")] - public void Constructor_CompatOverload_CreatesSuccessfully() - { - var options = new DiffPipelineOptions(); - var mockDiffer = new Mock(); - var mockProgress = new Mock>(); - var pipeline = new DiffPipeline(options, mockDiffer.Object, mockProgress.Object); - Assert.NotNull(pipeline); - } - - [Fact(DisplayName = "CleanAsync_sourcePath为null_抛出ArgumentNullException")] - public async Task CleanAsync_NullSourcePath_ThrowsArgumentNullException() - { - var pipeline = new DiffPipeline(); - await Assert.ThrowsAsync(() => - pipeline.CleanAsync(null!, "target", "patch")); - } - - [Fact(DisplayName = "CleanAsync_sourcePath为空白_抛出ArgumentNullException")] - public async Task CleanAsync_WhitespaceSourcePath_ThrowsArgumentNullException() - { - var pipeline = new DiffPipeline(); - await Assert.ThrowsAsync(() => - pipeline.CleanAsync(" ", "target", "patch")); - } - - [Fact(DisplayName = "CleanAsync_targetPath为null_抛出ArgumentNullException")] - public async Task CleanAsync_NullTargetPath_ThrowsArgumentNullException() - { - var pipeline = new DiffPipeline(); - await Assert.ThrowsAsync(() => - pipeline.CleanAsync("source", null!, "patch")); - } - - [Fact(DisplayName = "CleanAsync_patchPath为null_抛出ArgumentNullException")] - public async Task CleanAsync_NullPatchPath_ThrowsArgumentNullException() - { - var pipeline = new DiffPipeline(); - await Assert.ThrowsAsync(() => - pipeline.CleanAsync("source", "target", null!)); - } - - [Fact(DisplayName = "CleanAsync_目录不存在_抛出DirectoryNotFoundException")] - public async Task CleanAsync_NonExistentDir_ThrowsDirectoryNotFoundException() - { - var pipeline = new DiffPipeline(); - await Assert.ThrowsAsync(() => - pipeline.CleanAsync( - Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")), - Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")), - Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")))); - } - - [Fact(DisplayName = "CleanAsync_CancellationToken已取消_抛出OperationCanceledException")] - public async Task CleanAsync_CancelledToken_ThrowsOperationCanceledException() - { - var pipeline = new DiffPipeline(); - using var cts = new CancellationTokenSource(); - cts.Cancel(); - await Assert.ThrowsAsync(() => - pipeline.CleanAsync("src", "tgt", "patch", cancellationToken: cts.Token)); - } - - [Fact(DisplayName = "DirtyAsync_CancellationToken已取消_抛出OperationCanceledException")] - public async Task DirtyAsync_CancelledToken_ThrowsOperationCanceledException() - { - var pipeline = new DiffPipeline(); - using var cts = new CancellationTokenSource(); - cts.Cancel(); - await Assert.ThrowsAsync(() => - pipeline.DirtyAsync("app", "patch", cancellationToken: cts.Token)); - } - } -} diff --git a/src/c#/DrivelutionTest/Core/DrivelutionFactoryTests.cs b/src/c#/DrivelutionTest/Core/DrivelutionFactoryTests.cs deleted file mode 100644 index dc1ff6ff..00000000 --- a/src/c#/DrivelutionTest/Core/DrivelutionFactoryTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -using GeneralUpdate.Drivelution.Core; -using GeneralUpdate.Drivelution.Abstractions; -using GeneralUpdate.Drivelution.Abstractions.Configuration; - -namespace DrivelutionTest.Core; - -/// -/// DrivelutionFactory 测试 -/// 分支覆盖点: -/// - Create() 返回正确的平台实现 -/// - Create(DrivelutionOptions) 传递选项 -/// - CreateValidator() 返回正确平台验证器 -/// - CreateBackup() 返回正确平台备份实现 -/// - GetCurrentPlatform() 返回平台名称字符串 -/// - IsPlatformSupported() 返回布尔值 -/// - 不支持的平台抛出 PlatformNotSupportedException -/// 触发条件:调用工厂方法 -/// 预期结果:根据当前平台返回正确实现 -/// -public class DrivelutionFactoryTests -{ - [Fact(DisplayName = "DrivelutionFactory_Create_返回IGeneralDrivelution实例")] - public void Create_ReturnsIGeneralDrivelution() - { - var updater = DrivelutionFactory.Create(); - - Assert.NotNull(updater); - Assert.IsAssignableFrom(updater); - } - - [Fact(DisplayName = "DrivelutionFactory_Create_带Options参数返回实例")] - public void Create_WithOptions_ReturnsInstance() - { - var options = new DrivelutionOptions - { - DefaultRetryCount = 5, - DefaultTimeoutSeconds = 600 - }; - - var updater = DrivelutionFactory.Create(options); - - Assert.NotNull(updater); - Assert.IsAssignableFrom(updater); - } - - [Fact(DisplayName = "DrivelutionFactory_Create_nullOptions不抛异常")] - public void Create_NullOptions_DoesNotThrow() - { - var updater = DrivelutionFactory.Create(null); - - Assert.NotNull(updater); - } - - [Fact(DisplayName = "DrivelutionFactory_CreateValidator_返回IDriverValidator实例")] - public void CreateValidator_ReturnsIDriverValidator() - { - var validator = DrivelutionFactory.CreateValidator(); - - Assert.NotNull(validator); - Assert.IsAssignableFrom(validator); - } - - [Fact(DisplayName = "DrivelutionFactory_CreateBackup_返回IDriverBackup实例")] - public void CreateBackup_ReturnsIDriverBackup() - { - var backup = DrivelutionFactory.CreateBackup(); - - Assert.NotNull(backup); - Assert.IsAssignableFrom(backup); - } - - [Fact(DisplayName = "DrivelutionFactory_GetCurrentPlatform_返回非空字符串")] - public void GetCurrentPlatform_ReturnsNonNullString() - { - var platform = DrivelutionFactory.GetCurrentPlatform(); - - Assert.NotNull(platform); - Assert.NotEmpty(platform); - } - - [Fact(DisplayName = "DrivelutionFactory_IsPlatformSupported_返回true或false")] - public void IsPlatformSupported_ReturnsBoolean() - { - var supported = DrivelutionFactory.IsPlatformSupported(); - - // On Windows, Linux, or macOS, should return true - // This test is platform-dependent but always returns bool - Assert.True(supported || !supported); - } - - [Fact(DisplayName = "DrivelutionFactory_GetCurrentPlatform_返回Windows_Linux_MacOS或Unknown")] - public void GetCurrentPlatform_ReturnsKnownValue() - { - var platform = DrivelutionFactory.GetCurrentPlatform(); - - Assert.Contains(platform, new[] { "Windows", "Linux", "MacOS", "Unknown" }); - } - - [Fact(DisplayName = "DrivelutionFactory_Create_返回不同类型实例_验证非同一引用")] - public void Create_TwoCalls_ReturnDifferentInstances() - { - var u1 = DrivelutionFactory.Create(); - var u2 = DrivelutionFactory.Create(); - - Assert.NotSame(u1, u2); - } - - [Fact(DisplayName = "DrivelutionFactory_CreateValidator_两次调用返回不同实例")] - public void CreateValidator_TwoCalls_DifferentInstances() - { - var v1 = DrivelutionFactory.CreateValidator(); - var v2 = DrivelutionFactory.CreateValidator(); - - Assert.NotSame(v1, v2); - } -} diff --git a/src/c#/DrivelutionTest/DrivelutionTest.csproj b/src/c#/DrivelutionTest/DrivelutionTest.csproj deleted file mode 100644 index 4d7194ef..00000000 --- a/src/c#/DrivelutionTest/DrivelutionTest.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - net10.0 - enable - enable - false - true - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - - - - - - diff --git a/src/c#/DrivelutionTest/Execution/CommandResultTests.cs b/src/c#/DrivelutionTest/Execution/CommandResultTests.cs deleted file mode 100644 index e4f8c7c2..00000000 --- a/src/c#/DrivelutionTest/Execution/CommandResultTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -using GeneralUpdate.Drivelution.Core.Execution; - -namespace DrivelutionTest.Execution; - -/// -/// CommandResult 测试 -/// 分支覆盖点: -/// - 默认构造函数属性默认值 -/// - Success 属性:ExitCode == 0 返回 true,ExitCode != 0 返回 false -/// - ExitCode 正整数/负整数边界 -/// - StandardOutput/StandardError 字符串 -/// - ToString:成功时显示 Output,失败时显示 Error -/// 触发条件:创建 CommandResult 并设置属性 -/// 预期结果:逻辑正确 -/// -public class CommandResultTests -{ - [Fact(DisplayName = "CommandResult_默认构造函数_所有属性为默认值")] - public void CommandResult_DefaultConstructor_AllPropertiesHaveDefaultValues() - { - var result = new CommandResult(); - - Assert.Equal(0, result.ExitCode); - Assert.Equal(string.Empty, result.StandardOutput); - Assert.Equal(string.Empty, result.StandardError); - } - - [Fact(DisplayName = "CommandResult_Success_ExitCode为0时返回true")] - public void CommandResult_Success_ExitCodeZero_ReturnsTrue() - { - var result = new CommandResult { ExitCode = 0 }; - Assert.True(result.Success); - } - - [Fact(DisplayName = "CommandResult_Success_ExitCode为1时返回false")] - public void CommandResult_Success_ExitCodeOne_ReturnsFalse() - { - var result = new CommandResult { ExitCode = 1 }; - Assert.False(result.Success); - } - - [Theory(DisplayName = "CommandResult_Success_ExitCode不为零时全返回false")] - [InlineData(-1)] - [InlineData(2)] - [InlineData(255)] - [InlineData(int.MaxValue)] - [InlineData(int.MinValue)] - public void CommandResult_Success_NonZeroExitCode_ReturnsFalse(int exitCode) - { - var result = new CommandResult { ExitCode = exitCode }; - Assert.False(result.Success); - } - - [Fact(DisplayName = "CommandResult_ToString_成功时包含Output")] - public void CommandResult_ToString_Success_ContainsOutput() - { - var result = new CommandResult - { - ExitCode = 0, - StandardOutput = "Driver installed successfully\n" - }; - - var str = result.ToString(); - Assert.Contains("ExitCode=0", str); - Assert.Contains("Driver installed successfully", str); - Assert.DoesNotContain("Error=", str); - } - - [Fact(DisplayName = "CommandResult_ToString_失败时包含Error")] - public void CommandResult_ToString_Failure_ContainsError() - { - var result = new CommandResult - { - ExitCode = 1, - StandardError = "Permission denied\n" - }; - - var str = result.ToString(); - Assert.Contains("ExitCode=1", str); - Assert.Contains("Error=Permission denied", str); - } - - [Fact(DisplayName = "CommandResult_ToString_StandardOutput有前后空白时被Trim")] - public void CommandResult_ToString_StandardOutput_Trimmed() - { - var result = new CommandResult - { - ExitCode = 0, - StandardOutput = " output \n" - }; - - var str = result.ToString(); - Assert.Contains("Output=output", str); - } - - [Fact(DisplayName = "CommandResult_ToString_StandardError有前后空白时被Trim")] - public void CommandResult_ToString_StandardError_Trimmed() - { - var result = new CommandResult - { - ExitCode = 2, - StandardError = " error \n" - }; - - var str = result.ToString(); - Assert.Contains("Error=error", str); - } - - [Fact(DisplayName = "CommandResult_ToString_Output为空字符串时正常处理")] - public void CommandResult_ToString_EmptyOutput_Works() - { - var result = new CommandResult - { - ExitCode = 0, - StandardOutput = "" - }; - - var str = result.ToString(); - Assert.Contains("Output=", str); - } - - [Fact(DisplayName = "CommandResult_ToString_Error为空字符串时正常处理")] - public void CommandResult_ToString_EmptyError_Works() - { - var result = new CommandResult - { - ExitCode = 1, - StandardError = "" - }; - - var str = result.ToString(); - Assert.Contains("Error=", str); - } -} diff --git a/src/c#/DrivelutionTest/LinuxImplementations/LinuxDriverBackupTests.cs b/src/c#/DrivelutionTest/LinuxImplementations/LinuxDriverBackupTests.cs deleted file mode 100644 index 53350c37..00000000 --- a/src/c#/DrivelutionTest/LinuxImplementations/LinuxDriverBackupTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using GeneralUpdate.Drivelution.Linux.Implementation; -using GeneralUpdate.Drivelution.Abstractions.Exceptions; - -namespace DrivelutionTest.LinuxImplementations; - -public class LinuxDriverBackupTests : IDisposable -{ - private readonly string _tempDir; - private readonly LinuxDriverBackup _backup; - - public LinuxDriverBackupTests() - { - _tempDir = Path.Combine(Path.GetTempPath(), $"linux_drv_test_{Guid.NewGuid():N}"); - Directory.CreateDirectory(_tempDir); - _backup = new LinuxDriverBackup(); - } - - public void Dispose() - { - try { if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); } - catch { } - } - - [Fact(DisplayName = "LinuxDriverBackup_BackupAsync_源文件不存在_抛出FileNotFoundException")] - public async Task BackupAsync_SourceNotExists_ThrowsFileNotFoundException() - { - await Assert.ThrowsAsync(() => _backup.BackupAsync(Path.Combine(_tempDir, "nonexistent.ko"), Path.Combine(_tempDir, "backup"))); - } - - [Fact(DisplayName = "LinuxDriverBackup_BackupAsync_成功备份_返回true")] - public async Task BackupAsync_SuccessfulBackup_ReturnsTrue() - { - var sourceFile = Path.Combine(_tempDir, "test.ko"); - await File.WriteAllTextAsync(sourceFile, "module data"); - var result = await _backup.BackupAsync(sourceFile, Path.Combine(_tempDir, "backups", "driver_backup")); - Assert.True(result); - } - - [Fact(DisplayName = "LinuxDriverBackup_RestoreAsync_备份文件不存在_抛出FileNotFoundException")] - public async Task RestoreAsync_BackupNotExists_ThrowsFileNotFoundException() - { - await Assert.ThrowsAsync(() => _backup.RestoreAsync(Path.Combine(_tempDir, "nobackup.ko"), Path.Combine(_tempDir, "target.ko"))); - } - - [Fact(DisplayName = "LinuxDriverBackup_RestoreAsync_成功恢复_返回true")] - public async Task RestoreAsync_SuccessfulRestore_ReturnsTrue() - { - var backupFile = Path.Combine(_tempDir, "backup_module.ko"); - await File.WriteAllTextAsync(backupFile, "backup data"); - var targetFile = Path.Combine(_tempDir, "restored_module.ko"); - var result = await _backup.RestoreAsync(backupFile, targetFile); - Assert.True(result); - } - - [Fact(DisplayName = "LinuxDriverBackup_DeleteBackupAsync_文件存在_返回true")] - public async Task DeleteBackupAsync_FileExists_ReturnsTrue() - { - var file = Path.Combine(_tempDir, "to_delete.ko"); - await File.WriteAllTextAsync(file, "data"); - var result = await _backup.DeleteBackupAsync(file); - Assert.True(result); - } - - [Fact(DisplayName = "LinuxDriverBackup_DeleteBackupAsync_文件不存在_返回false")] - public async Task DeleteBackupAsync_FileNotExists_ReturnsFalse() - { - var result = await _backup.DeleteBackupAsync(Path.Combine(_tempDir, "nonexistent.ko")); - Assert.False(result); - } -} diff --git a/src/c#/DrivelutionTest/Models/DriverInfoTests.cs b/src/c#/DrivelutionTest/Models/DriverInfoTests.cs deleted file mode 100644 index 820e4b0c..00000000 --- a/src/c#/DrivelutionTest/Models/DriverInfoTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -using GeneralUpdate.Drivelution.Abstractions.Models; - -namespace DrivelutionTest.Models; - -/// -/// DriverInfo 测试 -/// 分支覆盖点: -/// - 默认构造函数:所有属性应为其默认值 -/// - 属性设置/获取:所有可写属性 -/// - 空字符串、空集合、默认值边界 -/// 触发条件:创建 DriverInfo 实例 -/// 预期结果:属性值正确返回 -/// -public class DriverInfoTests -{ - [Fact(DisplayName = "DriverInfo_默认构造函数_所有属性为默认值")] - public void DriverInfo_DefaultConstructor_AllPropertiesHaveDefaultValues() - { - // Act - var info = new DriverInfo(); - - // Assert - Assert.Equal(string.Empty, info.Name); - Assert.Equal(string.Empty, info.Version); - Assert.Equal(string.Empty, info.FilePath); - Assert.Equal(string.Empty, info.TargetOS); - Assert.Equal(string.Empty, info.Architecture); - Assert.Equal(string.Empty, info.HardwareId); - Assert.Equal(string.Empty, info.Hash); - Assert.Equal("SHA256", info.HashAlgorithm); - Assert.NotNull(info.TrustedPublishers); - Assert.Empty(info.TrustedPublishers); - Assert.Equal(string.Empty, info.Description); - Assert.Equal(default, info.ReleaseDate); - Assert.NotNull(info.Metadata); - Assert.Empty(info.Metadata); - } - - [Fact(DisplayName = "DriverInfo_设置Name属性_值正确返回")] - public void DriverInfo_SetName_ReturnsCorrectValue() - { - var info = new DriverInfo { Name = "Test Driver" }; - Assert.Equal("Test Driver", info.Name); - } - - [Fact(DisplayName = "DriverInfo_设置Version属性_值正确返回")] - public void DriverInfo_SetVersion_ReturnsCorrectValue() - { - var info = new DriverInfo { Version = "2.1.0" }; - Assert.Equal("2.1.0", info.Version); - } - - [Fact(DisplayName = "DriverInfo_设置FilePath属性_值正确返回")] - public void DriverInfo_SetFilePath_ReturnsCorrectValue() - { - var info = new DriverInfo { FilePath = "C:\\drivers\\test.sys" }; - Assert.Equal("C:\\drivers\\test.sys", info.FilePath); - } - - [Fact(DisplayName = "DriverInfo_设置TargetOS属性_值正确返回")] - public void DriverInfo_SetTargetOS_ReturnsCorrectValue() - { - var info = new DriverInfo { TargetOS = "Windows" }; - Assert.Equal("Windows", info.TargetOS); - } - - [Fact(DisplayName = "DriverInfo_设置Architecture属性_值正确返回")] - public void DriverInfo_SetArchitecture_ReturnsCorrectValue() - { - var info = new DriverInfo { Architecture = "x64" }; - Assert.Equal("x64", info.Architecture); - } - - [Fact(DisplayName = "DriverInfo_设置HardwareId属性_值正确返回")] - public void DriverInfo_SetHardwareId_ReturnsCorrectValue() - { - var info = new DriverInfo { HardwareId = "PCI\\VEN_8086" }; - Assert.Equal("PCI\\VEN_8086", info.HardwareId); - } - - [Fact(DisplayName = "DriverInfo_设置Hash属性_值正确返回")] - public void DriverInfo_SetHash_ReturnsCorrectValue() - { - var info = new DriverInfo { Hash = "abc123" }; - Assert.Equal("abc123", info.Hash); - } - - [Fact(DisplayName = "DriverInfo_设置HashAlgorithm属性_值正确返回")] - public void DriverInfo_SetHashAlgorithm_ReturnsCorrectValue() - { - var info = new DriverInfo { HashAlgorithm = "MD5" }; - Assert.Equal("MD5", info.HashAlgorithm); - } - - [Fact(DisplayName = "DriverInfo_TrustedPublishers_可以添加和检索发布者")] - public void DriverInfo_TrustedPublishers_CanAddAndRetrievePublishers() - { - var info = new DriverInfo(); - info.TrustedPublishers.Add("Microsoft"); - info.TrustedPublishers.Add("Intel"); - - Assert.Equal(2, info.TrustedPublishers.Count); - Assert.Contains("Microsoft", info.TrustedPublishers); - Assert.Contains("Intel", info.TrustedPublishers); - } - - [Fact(DisplayName = "DriverInfo_设置Description属性_值正确返回")] - public void DriverInfo_SetDescription_ReturnsCorrectValue() - { - var info = new DriverInfo { Description = "Network adapter driver" }; - Assert.Equal("Network adapter driver", info.Description); - } - - [Fact(DisplayName = "DriverInfo_设置ReleaseDate属性_值正确返回")] - public void DriverInfo_SetReleaseDate_ReturnsCorrectValue() - { - var date = new DateTime(2025, 6, 15); - var info = new DriverInfo { ReleaseDate = date }; - Assert.Equal(date, info.ReleaseDate); - } - - [Fact(DisplayName = "DriverInfo_Metadata_可以添加和检索键值对")] - public void DriverInfo_Metadata_CanAddAndRetrieveKeyValuePairs() - { - var info = new DriverInfo(); - info.Metadata["Author"] = "JusterZhu"; - info.Metadata["Platform"] = "Windows"; - - Assert.Equal(2, info.Metadata.Count); - Assert.Equal("JusterZhu", info.Metadata["Author"]); - Assert.Equal("Windows", info.Metadata["Platform"]); - } - - [Fact(DisplayName = "DriverInfo_TrustedPublishers_空列表_Count为0")] - public void DriverInfo_TrustedPublishers_EmptyList_CountIsZero() - { - var info = new DriverInfo(); - Assert.Empty(info.TrustedPublishers); - Assert.Equal(0, info.TrustedPublishers.Count); - } - - [Fact(DisplayName = "DriverInfo_Name为空字符串时_不会抛出异常")] - public void DriverInfo_NameIsEmptyString_DoesNotThrow() - { - var info = new DriverInfo { Name = "" }; - Assert.Equal(string.Empty, info.Name); - } -} diff --git a/src/c#/DrivelutionTest/Models/ErrorInfoTests.cs b/src/c#/DrivelutionTest/Models/ErrorInfoTests.cs deleted file mode 100644 index b2c5a116..00000000 --- a/src/c#/DrivelutionTest/Models/ErrorInfoTests.cs +++ /dev/null @@ -1,149 +0,0 @@ -using GeneralUpdate.Drivelution.Abstractions.Models; - -namespace DrivelutionTest.Models; - -/// -/// ErrorInfo 测试 -/// 分支覆盖点: -/// - 默认构造函数:所有属性默认值 -/// - 属性赋值:Code, Type, Message, Details, StackTrace, Timestamp, CanRetry, SuggestedResolution -/// - 可空属性:StackTrace为null, InnerException为null -/// - 枚举值遍历:ErrorType所有成员 -/// 触发条件:创建 ErrorInfo 实例并设置属性 -/// 预期结果:属性值正确返回 -/// -public class ErrorInfoTests -{ - [Fact(DisplayName = "ErrorInfo_默认构造函数_所有属性为默认值")] - public void ErrorInfo_DefaultConstructor_AllPropertiesHaveDefaultValues() - { - var error = new ErrorInfo(); - - Assert.Equal(string.Empty, error.Code); - Assert.Equal(default(ErrorType), error.Type); - Assert.Equal(string.Empty, error.Message); - Assert.Equal(string.Empty, error.Details); - Assert.Null(error.StackTrace); - Assert.Null(error.InnerException); - Assert.Equal(default, error.CanRetry); - Assert.Equal(string.Empty, error.SuggestedResolution); - } - - [Fact(DisplayName = "ErrorInfo_Timestamp_默认值为UtcNow附近")] - public void ErrorInfo_Timestamp_DefaultIsNearUtcNow() - { - var before = DateTime.UtcNow.AddSeconds(-1); - var error = new ErrorInfo(); - var after = DateTime.UtcNow.AddSeconds(1); - - Assert.InRange(error.Timestamp, before, after); - } - - [Fact(DisplayName = "ErrorInfo_设置Code属性_值正确返回")] - public void ErrorInfo_SetCode_ReturnsCorrectValue() - { - var error = new ErrorInfo { Code = "ERR_TIMEOUT" }; - Assert.Equal("ERR_TIMEOUT", error.Code); - } - - [Fact(DisplayName = "ErrorInfo_设置Type为PermissionDenied_值正确返回")] - public void ErrorInfo_SetTypePermissionDenied_ReturnsCorrectValue() - { - var error = new ErrorInfo { Type = ErrorType.PermissionDenied }; - Assert.Equal(ErrorType.PermissionDenied, error.Type); - } - - [Theory(DisplayName = "ErrorInfo_Type枚举_所有值均可设置")] - [InlineData(ErrorType.PermissionDenied)] - [InlineData(ErrorType.SignatureValidationFailed)] - [InlineData(ErrorType.HashValidationFailed)] - [InlineData(ErrorType.CompatibilityValidationFailed)] - [InlineData(ErrorType.FileNotFound)] - [InlineData(ErrorType.FileCorrupted)] - [InlineData(ErrorType.BackupFailed)] - [InlineData(ErrorType.InstallationFailed)] - [InlineData(ErrorType.RollbackFailed)] - [InlineData(ErrorType.NetworkError)] - [InlineData(ErrorType.Timeout)] - [InlineData(ErrorType.UserCancelled)] - [InlineData(ErrorType.SystemNotSupported)] - [InlineData(ErrorType.Unknown)] - public void ErrorInfo_Type_AllEnumValuesCanBeSet(ErrorType type) - { - var error = new ErrorInfo { Type = type }; - Assert.Equal(type, error.Type); - } - - [Fact(DisplayName = "ErrorInfo_设置Message属性_值正确返回")] - public void ErrorInfo_SetMessage_ReturnsCorrectValue() - { - var error = new ErrorInfo { Message = "Operation timed out" }; - Assert.Equal("Operation timed out", error.Message); - } - - [Fact(DisplayName = "ErrorInfo_设置Details属性_值正确返回")] - public void ErrorInfo_SetDetails_ReturnsCorrectValue() - { - var error = new ErrorInfo { Details = "Detailed error information" }; - Assert.Equal("Detailed error information", error.Details); - } - - [Fact(DisplayName = "ErrorInfo_StackTrace为null时_不抛出异常")] - public void ErrorInfo_StackTraceIsNull_DoesNotThrow() - { - var error = new ErrorInfo { StackTrace = null }; - Assert.Null(error.StackTrace); - } - - [Fact(DisplayName = "ErrorInfo_设置StackTrace属性_值正确返回")] - public void ErrorInfo_SetStackTrace_ReturnsCorrectValue() - { - var error = new ErrorInfo { StackTrace = "at Test.Method()" }; - Assert.Equal("at Test.Method()", error.StackTrace); - } - - [Fact(DisplayName = "ErrorInfo_InnerException为null时_不抛出异常")] - public void ErrorInfo_InnerExceptionIsNull_DoesNotThrow() - { - var error = new ErrorInfo { InnerException = null }; - Assert.Null(error.InnerException); - } - - [Fact(DisplayName = "ErrorInfo_设置InnerException属性_值正确返回")] - public void ErrorInfo_SetInnerException_ReturnsCorrectValue() - { - var ex = new InvalidOperationException("test"); - var error = new ErrorInfo { InnerException = ex }; - Assert.Same(ex, error.InnerException); - } - - [Theory(DisplayName = "ErrorInfo_CanRetry_两种值均可设置")] - [InlineData(true)] - [InlineData(false)] - public void ErrorInfo_CanRetry_BothValuesCanBeSet(bool canRetry) - { - var error = new ErrorInfo { CanRetry = canRetry }; - Assert.Equal(canRetry, error.CanRetry); - } - - [Fact(DisplayName = "ErrorInfo_设置SuggestedResolution属性_值正确返回")] - public void ErrorInfo_SetSuggestedResolution_ReturnsCorrectValue() - { - var error = new ErrorInfo { SuggestedResolution = "Restart the application" }; - Assert.Equal("Restart the application", error.SuggestedResolution); - } - - [Fact(DisplayName = "ErrorInfo_空字符串Code_不抛出异常")] - public void ErrorInfo_EmptyStringCode_DoesNotThrow() - { - var error = new ErrorInfo { Code = "" }; - Assert.Equal(string.Empty, error.Code); - } - - [Fact(DisplayName = "ErrorInfo_空字符串Message_不抛出异常")] - public void ErrorInfo_EmptyStringMessage_DoesNotThrow() - { - var error = new ErrorInfo { Message = "" }; - Assert.Equal(string.Empty, error.Message); - } -} diff --git a/src/c#/DrivelutionTest/Models/UpdateResultTests.cs b/src/c#/DrivelutionTest/Models/UpdateResultTests.cs deleted file mode 100644 index 58f00cb4..00000000 --- a/src/c#/DrivelutionTest/Models/UpdateResultTests.cs +++ /dev/null @@ -1,146 +0,0 @@ -using GeneralUpdate.Drivelution.Abstractions.Models; - -namespace DrivelutionTest.Models; - -/// -/// UpdateResult 测试 -/// 分支覆盖点: -/// - 默认构造函数属性默认值 -/// - DurationMs 计算:EndTime > StartTime 返回正值,EndTime == StartTime 返回0 -/// - StepLogs 列表为空,可添加 -/// - Error 为 null 和 非 null -/// - BackupPath 为 null -/// - RolledBack 默认为 false -/// - Status 枚举所有值 -/// 触发条件:创建 UpdateResult 并设置属性 -/// 预期结果:属性正确返回 -/// -public class UpdateResultTests -{ - [Fact(DisplayName = "UpdateResult_默认构造函数_所有属性为默认值")] - public void UpdateResult_DefaultConstructor_AllPropertiesHaveDefaultValues() - { - var result = new UpdateResult(); - - Assert.False(result.Success); - Assert.Equal(default(UpdateStatus), result.Status); - Assert.Null(result.Error); - Assert.Equal(default, result.StartTime); - Assert.Equal(default, result.EndTime); - Assert.Null(result.BackupPath); - Assert.False(result.RolledBack); - Assert.Equal(string.Empty, result.Message); - Assert.NotNull(result.StepLogs); - Assert.Empty(result.StepLogs); - } - - [Fact(DisplayName = "UpdateResult_DurationMs_EndTime大于StartTime_返回正毫秒数")] - public void UpdateResult_DurationMs_EndTimeAfterStartTime_ReturnsPositiveMs() - { - var result = new UpdateResult - { - StartTime = new DateTime(2025, 1, 1, 12, 0, 0), - EndTime = new DateTime(2025, 1, 1, 12, 0, 5) - }; - - Assert.Equal(5000, result.DurationMs); - } - - [Fact(DisplayName = "UpdateResult_DurationMs_EndTime等于StartTime_返回0")] - public void UpdateResult_DurationMs_EndTimeEqualsStartTime_ReturnsZero() - { - var time = DateTime.UtcNow; - var result = new UpdateResult - { - StartTime = time, - EndTime = time - }; - - Assert.Equal(0, result.DurationMs); - } - - [Fact(DisplayName = "UpdateResult_Success为true_值正确返回")] - public void UpdateResult_SuccessIsTrue_ReturnsTrue() - { - var result = new UpdateResult { Success = true }; - Assert.True(result.Success); - } - - [Fact(DisplayName = "UpdateResult_Status为Succeeded_值正确返回")] - public void UpdateResult_StatusIsSucceeded_ReturnsSucceeded() - { - var result = new UpdateResult { Status = UpdateStatus.Succeeded }; - Assert.Equal(UpdateStatus.Succeeded, result.Status); - } - - [Fact(DisplayName = "UpdateResult_Error不为null_值正确返回")] - public void UpdateResult_ErrorNotNull_ReturnsErrorInfo() - { - var error = new ErrorInfo { Code = "ERR_TEST" }; - var result = new UpdateResult { Error = error }; - - Assert.NotNull(result.Error); - Assert.Equal("ERR_TEST", result.Error.Code); - } - - [Fact(DisplayName = "UpdateResult_Error为null_不抛出异常")] - public void UpdateResult_ErrorIsNull_DoesNotThrow() - { - var result = new UpdateResult { Error = null }; - Assert.Null(result.Error); - } - - [Fact(DisplayName = "UpdateResult_BackupPath为null_不抛出异常")] - public void UpdateResult_BackupPathIsNull_DoesNotThrow() - { - var result = new UpdateResult { BackupPath = null }; - Assert.Null(result.BackupPath); - } - - [Fact(DisplayName = "UpdateResult_BackupPath有值_返回正确路径")] - public void UpdateResult_BackupPathHasValue_ReturnsCorrectPath() - { - var result = new UpdateResult { BackupPath = "C:\\backups\\driver" }; - Assert.Equal("C:\\backups\\driver", result.BackupPath); - } - - [Fact(DisplayName = "UpdateResult_RolledBack为true_值正确返回")] - public void UpdateResult_RolledBackIsTrue_ReturnsTrue() - { - var result = new UpdateResult { RolledBack = true }; - Assert.True(result.RolledBack); - } - - [Fact(DisplayName = "UpdateResult_StepLogs_可以添加日志条目")] - public void UpdateResult_StepLogs_CanAddLogEntries() - { - var result = new UpdateResult(); - result.StepLogs.Add("[12:00:00] Step 1 completed"); - result.StepLogs.Add("[12:00:05] Step 2 completed"); - - Assert.Equal(2, result.StepLogs.Count); - Assert.Contains("[12:00:00] Step 1 completed", result.StepLogs); - } - - [Fact(DisplayName = "UpdateResult_Message_可设置信息消息")] - public void UpdateResult_Message_CanSetInfoMessage() - { - var result = new UpdateResult { Message = "Update completed successfully" }; - Assert.Equal("Update completed successfully", result.Message); - } - - [Theory(DisplayName = "UpdateResult_Status_所有枚举值均可设置")] - [InlineData(UpdateStatus.NotStarted)] - [InlineData(UpdateStatus.Validating)] - [InlineData(UpdateStatus.BackingUp)] - [InlineData(UpdateStatus.Updating)] - [InlineData(UpdateStatus.Verifying)] - [InlineData(UpdateStatus.Succeeded)] - [InlineData(UpdateStatus.Failed)] - [InlineData(UpdateStatus.RolledBack)] - public void UpdateResult_Status_AllEnumValuesCanBeSet(UpdateStatus status) - { - var result = new UpdateResult { Status = status }; - Assert.Equal(status, result.Status); - } -} diff --git a/src/c#/DrivelutionTest/Models/UpdateStrategyTests.cs b/src/c#/DrivelutionTest/Models/UpdateStrategyTests.cs deleted file mode 100644 index 78920d29..00000000 --- a/src/c#/DrivelutionTest/Models/UpdateStrategyTests.cs +++ /dev/null @@ -1,166 +0,0 @@ -using GeneralUpdate.Drivelution.Abstractions.Models; - -namespace DrivelutionTest.Models; - -/// -/// UpdateStrategy 测试 -/// 分支覆盖点: -/// - 默认构造函数:所有属性默认值 -/// - Mode 枚举:Full, Incremental -/// - ForceUpdate 布尔值 -/// - RequireBackup 布尔值(默认true) -/// - BackupPath 字符串 -/// - RetryCount 整数值(包括0, 负数, 极值) -/// - RetryIntervalSeconds 整数值 -/// - Priority 整数值 -/// - RestartMode 枚举:None, Prompt, Delayed, Immediate -/// - SkipSignatureValidation 布尔值(默认false) -/// - SkipHashValidation 布尔值(默认false) -/// - TimeoutSeconds 整数值(默认300) -/// 触发条件:创建 UpdateStrategy 并设置各属性 -/// 预期结果:属性值正确 -/// -public class UpdateStrategyTests -{ - [Fact(DisplayName = "UpdateStrategy_默认构造函数_所有属性为默认值")] - public void UpdateStrategy_DefaultConstructor_AllPropertiesHaveDefaultValues() - { - var strategy = new UpdateStrategy(); - - Assert.Equal(UpdateMode.Full, strategy.Mode); - Assert.False(strategy.ForceUpdate); - Assert.True(strategy.RequireBackup); - Assert.Equal(string.Empty, strategy.BackupPath); - Assert.Equal(3, strategy.RetryCount); - Assert.Equal(5, strategy.RetryIntervalSeconds); - Assert.Equal(0, strategy.Priority); - Assert.Equal(RestartMode.Prompt, strategy.RestartMode); - Assert.False(strategy.SkipSignatureValidation); - Assert.False(strategy.SkipHashValidation); - Assert.Equal(300, strategy.TimeoutSeconds); - } - - [Theory(DisplayName = "UpdateStrategy_Mode_两种枚举值均可设置")] - [InlineData(UpdateMode.Full)] - [InlineData(UpdateMode.Incremental)] - public void UpdateStrategy_Mode_BothEnumValuesCanBeSet(UpdateMode mode) - { - var strategy = new UpdateStrategy { Mode = mode }; - Assert.Equal(mode, strategy.Mode); - } - - [Theory(DisplayName = "UpdateStrategy_ForceUpdate_两种值均可设置")] - [InlineData(true)] - [InlineData(false)] - public void UpdateStrategy_ForceUpdate_BothValuesCanBeSet(bool force) - { - var strategy = new UpdateStrategy { ForceUpdate = force }; - Assert.Equal(force, strategy.ForceUpdate); - } - - [Theory(DisplayName = "UpdateStrategy_RequireBackup_两种值均可设置")] - [InlineData(true)] - [InlineData(false)] - public void UpdateStrategy_RequireBackup_BothValuesCanBeSet(bool requireBackup) - { - var strategy = new UpdateStrategy { RequireBackup = requireBackup }; - Assert.Equal(requireBackup, strategy.RequireBackup); - } - - [Fact(DisplayName = "UpdateStrategy_BackupPath_空字符串默认值")] - public void UpdateStrategy_BackupPath_DefaultIsEmptyString() - { - var strategy = new UpdateStrategy(); - Assert.Equal(string.Empty, strategy.BackupPath); - } - - [Fact(DisplayName = "UpdateStrategy_BackupPath_可设置路径")] - public void UpdateStrategy_BackupPath_CanSetPath() - { - var strategy = new UpdateStrategy { BackupPath = "C:\\backups" }; - Assert.Equal("C:\\backups", strategy.BackupPath); - } - - [Theory(DisplayName = "UpdateStrategy_RetryCount_各种整数值均可设置")] - [InlineData(0)] - [InlineData(1)] - [InlineData(5)] - [InlineData(100)] - [InlineData(-1)] - public void UpdateStrategy_RetryCount_VariousValuesCanBeSet(int count) - { - var strategy = new UpdateStrategy { RetryCount = count }; - Assert.Equal(count, strategy.RetryCount); - } - - [Theory(DisplayName = "UpdateStrategy_RetryIntervalSeconds_各种值均可设置")] - [InlineData(0)] - [InlineData(1)] - [InlineData(10)] - [InlineData(60)] - public void UpdateStrategy_RetryIntervalSeconds_VariousValuesCanBeSet(int seconds) - { - var strategy = new UpdateStrategy { RetryIntervalSeconds = seconds }; - Assert.Equal(seconds, strategy.RetryIntervalSeconds); - } - - [Theory(DisplayName = "UpdateStrategy_Priority_各种值均可设置")] - [InlineData(0)] - [InlineData(1)] - [InlineData(-1)] - [InlineData(int.MaxValue)] - [InlineData(int.MinValue)] - public void UpdateStrategy_Priority_VariousValuesCanBeSet(int priority) - { - var strategy = new UpdateStrategy { Priority = priority }; - Assert.Equal(priority, strategy.Priority); - } - - [Theory(DisplayName = "UpdateStrategy_RestartMode_所有枚举值均可设置")] - [InlineData(RestartMode.None)] - [InlineData(RestartMode.Prompt)] - [InlineData(RestartMode.Delayed)] - [InlineData(RestartMode.Immediate)] - public void UpdateStrategy_RestartMode_AllEnumValuesCanBeSet(RestartMode mode) - { - var strategy = new UpdateStrategy { RestartMode = mode }; - Assert.Equal(mode, strategy.RestartMode); - } - - [Theory(DisplayName = "UpdateStrategy_SkipSignatureValidation_两种值均可设置")] - [InlineData(true)] - [InlineData(false)] - public void UpdateStrategy_SkipSignatureValidation_BothValuesCanBeSet(bool skip) - { - var strategy = new UpdateStrategy { SkipSignatureValidation = skip }; - Assert.Equal(skip, strategy.SkipSignatureValidation); - } - - [Theory(DisplayName = "UpdateStrategy_SkipHashValidation_两种值均可设置")] - [InlineData(true)] - [InlineData(false)] - public void UpdateStrategy_SkipHashValidation_BothValuesCanBeSet(bool skip) - { - var strategy = new UpdateStrategy { SkipHashValidation = skip }; - Assert.Equal(skip, strategy.SkipHashValidation); - } - - [Theory(DisplayName = "UpdateStrategy_TimeoutSeconds_各种值均可设置")] - [InlineData(0)] - [InlineData(1)] - [InlineData(300)] - [InlineData(3600)] - [InlineData(int.MaxValue)] - public void UpdateStrategy_TimeoutSeconds_VariousValuesCanBeSet(int timeout) - { - var strategy = new UpdateStrategy { TimeoutSeconds = timeout }; - Assert.Equal(timeout, strategy.TimeoutSeconds); - } - - [Fact(DisplayName = "UpdateStrategy_TimeoutSeconds为0_表示无超时")] - public void UpdateStrategy_TimeoutSecondsIsZero_RepresentsNoTimeout() - { - var strategy = new UpdateStrategy { TimeoutSeconds = 0 }; - Assert.Equal(0, strategy.TimeoutSeconds); - } -} diff --git a/src/c#/DrivelutionTest/Pipeline/PipelineResultTests.cs b/src/c#/DrivelutionTest/Pipeline/PipelineResultTests.cs deleted file mode 100644 index c8d45b1a..00000000 --- a/src/c#/DrivelutionTest/Pipeline/PipelineResultTests.cs +++ /dev/null @@ -1,205 +0,0 @@ -using GeneralUpdate.Drivelution.Abstractions.Models; -using GeneralUpdate.Drivelution.Core.Pipeline; - -namespace DrivelutionTest.Pipeline; - -/// -/// PipelineResult 测试 -/// 分支覆盖点: -/// - Ok() 静态方法:Success=true, ErrorMessage=null, Exception=null -/// - Fail(string) 静态方法:Success=false, ErrorMessage已设置, Exception=null -/// - Fail(string, Exception) 静态方法:Success=false, 已设置 ErrorMessage 和 Exception -/// - 空字符串 ErrorMessage -/// - Exception 为 null -/// 触发条件:调用 Ok() / Fail() -/// 预期结果:属性正确反映成功/失败状态 -/// -public class PipelineResultTests -{ - [Fact(DisplayName = "PipelineResult_Ok_返回Success为true")] - public void PipelineResult_Ok_ReturnsSuccessTrue() - { - var result = PipelineResult.Ok(); - - Assert.True(result.Success); - Assert.Null(result.ErrorMessage); - Assert.Null(result.Exception); - } - - [Fact(DisplayName = "PipelineResult_Fail_仅含消息_返回Success为false")] - public void PipelineResult_Fail_MessageOnly_ReturnsSuccessFalse() - { - var result = PipelineResult.Fail("Something went wrong"); - - Assert.False(result.Success); - Assert.Equal("Something went wrong", result.ErrorMessage); - Assert.Null(result.Exception); - } - - [Fact(DisplayName = "PipelineResult_Fail_含消息和异常_返回Success为false")] - public void PipelineResult_Fail_MessageAndException_ReturnsSuccessFalse() - { - var ex = new InvalidOperationException("inner error"); - var result = PipelineResult.Fail("Failed", ex); - - Assert.False(result.Success); - Assert.Equal("Failed", result.ErrorMessage); - Assert.Same(ex, result.Exception); - } - - [Fact(DisplayName = "PipelineResult_Fail_空字符串消息_不抛出异常")] - public void PipelineResult_Fail_EmptyMessage_DoesNotThrow() - { - var result = PipelineResult.Fail(""); - - Assert.False(result.Success); - Assert.Equal("", result.ErrorMessage); - } - - [Fact(DisplayName = "PipelineResult_Fail_消息为null_允许null")] - public void PipelineResult_Fail_NullMessage_AllowsNull() - { - var result = PipelineResult.Fail(null!); - - Assert.False(result.Success); - Assert.Null(result.ErrorMessage); - } - - [Fact(DisplayName = "PipelineResult_Fail_Exception为null_不会包装")] - public void PipelineResult_Fail_NullException_DoesNotWrap() - { - var result = PipelineResult.Fail("Error", null); - - Assert.False(result.Success); - Assert.Equal("Error", result.ErrorMessage); - Assert.Null(result.Exception); - } - - [Fact(DisplayName = "PipelineResult_Ok_多次调用返回不同实例")] - public void PipelineResult_Ok_MultipleCallsReturnDifferentInstances() - { - var r1 = PipelineResult.Ok(); - var r2 = PipelineResult.Ok(); - - Assert.NotSame(r1, r2); - Assert.True(r1.Success); - Assert.True(r2.Success); - } -} - -/// -/// PipelineContext 测试 -/// 分支覆盖点: -/// - 构造函数正确初始化 DriverInfo, Strategy, Result -/// - 对 null 参数抛出 ArgumentNullException -/// - Bag 字典可读写 -/// - Bag 初始为空 -/// 触发条件:创建 PipelineContext -/// 预期结果:构造正确,null 检测生效 -/// -public class PipelineContextTests -{ - private static DriverInfo CreateDriver() => new() - { - Name = "TestDriver", - Version = "1.0.0", - FilePath = "/test/path" - }; - - private static UpdateStrategy CreateStrategy() => new() - { - RequireBackup = true, - BackupPath = "/backups" - }; - - private static UpdateResult CreateResult() => new() - { - Status = UpdateStatus.NotStarted - }; - - [Fact(DisplayName = "PipelineContext_构造函数_正确初始化所有属性")] - public void PipelineContext_Constructor_InitializesAllProperties() - { - var driver = CreateDriver(); - var strategy = CreateStrategy(); - var result = CreateResult(); - - var context = new PipelineContext(driver, strategy, result); - - Assert.Same(driver, context.DriverInfo); - Assert.Same(strategy, context.Strategy); - Assert.Same(result, context.Result); - Assert.NotNull(context.Bag); - Assert.Empty(context.Bag); - } - - [Fact(DisplayName = "PipelineContext_DriverInfo为null_抛出ArgumentNullException")] - public void PipelineContext_DriverInfoNull_ThrowsArgumentNullException() - { - Assert.Throws(() => - new PipelineContext(null!, CreateStrategy(), CreateResult())); - } - - [Fact(DisplayName = "PipelineContext_Strategy为null_抛出ArgumentNullException")] - public void PipelineContext_StrategyNull_ThrowsArgumentNullException() - { - Assert.Throws(() => - new PipelineContext(CreateDriver(), null!, CreateResult())); - } - - [Fact(DisplayName = "PipelineContext_Result为null_抛出ArgumentNullException")] - public void PipelineContext_ResultNull_ThrowsArgumentNullException() - { - Assert.Throws(() => - new PipelineContext(CreateDriver(), CreateStrategy(), null!)); - } - - [Fact(DisplayName = "PipelineContext_Bag_可存储和检索值")] - public void PipelineContext_Bag_CanStoreAndRetrieveValues() - { - var context = new PipelineContext(CreateDriver(), CreateStrategy(), CreateResult()); - - context.Bag["key1"] = "value1"; - context.Bag["key2"] = 42; - context.Bag["BackupPath"] = "/backups/driver"; - - Assert.Equal(3, context.Bag.Count); - Assert.Equal("value1", context.Bag["key1"]); - Assert.Equal(42, context.Bag["key2"]); - Assert.Equal("/backups/driver", context.Bag["BackupPath"]); - } - - [Fact(DisplayName = "PipelineContext_Bag_可存储null值")] - public void PipelineContext_Bag_CanStoreNullValues() - { - var context = new PipelineContext(CreateDriver(), CreateStrategy(), CreateResult()); - - context.Bag["nullKey"] = null; - - Assert.True(context.Bag.ContainsKey("nullKey")); - Assert.Null(context.Bag["nullKey"]); - } - - [Fact(DisplayName = "PipelineContext_Bag_TryGetValue_获取不存在的键返回false")] - public void PipelineContext_Bag_TryGetValue_MissingKey_ReturnsFalse() - { - var context = new PipelineContext(CreateDriver(), CreateStrategy(), CreateResult()); - - var found = context.Bag.TryGetValue("nonexistent", out var value); - - Assert.False(found); - Assert.Null(value); - } - - [Fact(DisplayName = "PipelineContext_Bag_TryGetValue_获取存在的键返回true")] - public void PipelineContext_Bag_TryGetValue_ExistingKey_ReturnsTrue() - { - var context = new PipelineContext(CreateDriver(), CreateStrategy(), CreateResult()); - context.Bag["exists"] = "hello"; - - var found = context.Bag.TryGetValue("exists", out var value); - - Assert.True(found); - Assert.Equal("hello", value); - } -} diff --git a/src/c#/DrivelutionTest/Pipeline/RetryPolicyTests.cs b/src/c#/DrivelutionTest/Pipeline/RetryPolicyTests.cs deleted file mode 100644 index 9d602c3f..00000000 --- a/src/c#/DrivelutionTest/Pipeline/RetryPolicyTests.cs +++ /dev/null @@ -1,337 +0,0 @@ -using GeneralUpdate.Drivelution.Core.Pipeline; -using GeneralUpdate.Drivelution.Abstractions.Configuration; - -namespace DrivelutionTest.Pipeline; - -/// -/// RetryPolicy 测试 -/// 分支覆盖点: -/// - 构造函数:正确设置 MaxRetries, Delay, UseExponentialBackoff -/// - Default 静态属性:3次重试, 5秒间隔, 无回退 -/// - NoRetry 静态属性:0次重试, 零延迟 -/// - FromOptions: options为null返回Default, options值有效则构建, 值为0/负使用默认值 -/// - ExecuteAsync: 成功操作直接返回, 失败操作重试, OperationCanceledException不重试直接抛出 -/// - ExecuteWithRetryAsync: 成功返回true, 失败重试, 耗尽重试返回false -/// - 指数退避:Delay按2^(attempt-1)翻倍 -/// - 边界:MaxRetries=0时无重试 -/// 触发条件:构造和执行操作 -/// 预期结果:重试行为正确 -/// -public class RetryPolicyTests -{ - [Fact(DisplayName = "RetryPolicy_构造函数_正确设置属性")] - public void RetryPolicy_Constructor_SetsPropertiesCorrectly() - { - var policy = new RetryPolicy(5, TimeSpan.FromSeconds(10)); - - Assert.Equal(5, policy.MaxRetries); - Assert.Equal(TimeSpan.FromSeconds(10), policy.Delay); - Assert.False(policy.UseExponentialBackoff); - } - - [Fact(DisplayName = "RetryPolicy_构造函数_UseExponentialBackoff为true")] - public void RetryPolicy_Constructor_ExponentialBackoffTrue() - { - var policy = new RetryPolicy(3, TimeSpan.FromSeconds(1), true); - - Assert.Equal(3, policy.MaxRetries); - Assert.True(policy.UseExponentialBackoff); - } - - [Fact(DisplayName = "RetryPolicy_Default_返回3次重试5秒间隔")] - public void RetryPolicy_Default_Returns3Retries5Seconds() - { - var policy = RetryPolicy.Default; - - Assert.Equal(3, policy.MaxRetries); - Assert.Equal(TimeSpan.FromSeconds(5), policy.Delay); - Assert.False(policy.UseExponentialBackoff); - } - - [Fact(DisplayName = "RetryPolicy_NoRetry_返回0次重试零延迟")] - public void RetryPolicy_NoRetry_ReturnsZeroRetries() - { - var policy = RetryPolicy.NoRetry; - - Assert.Equal(0, policy.MaxRetries); - Assert.Equal(TimeSpan.Zero, policy.Delay); - } - - [Fact(DisplayName = "RetryPolicy_FromOptions_null参数返回Default")] - public void RetryPolicy_FromOptions_NullOptions_ReturnsDefault() - { - var policy = RetryPolicy.FromOptions(null); - - Assert.Equal(3, policy.MaxRetries); - Assert.Equal(TimeSpan.FromSeconds(5), policy.Delay); - } - - [Fact(DisplayName = "RetryPolicy_FromOptions_有效值返回对应策略")] - public void RetryPolicy_FromOptions_ValidOptions_ReturnsCorrespondingPolicy() - { - var options = new DrivelutionOptions - { - DefaultRetryCount = 10, - DefaultRetryIntervalSeconds = 15, - UseExponentialBackoff = true - }; - - var policy = RetryPolicy.FromOptions(options); - - Assert.Equal(10, policy.MaxRetries); - Assert.Equal(TimeSpan.FromSeconds(15), policy.Delay); - Assert.True(policy.UseExponentialBackoff); - } - - [Fact(DisplayName = "RetryPolicy_FromOptions_RetryCount为0使用默认3")] - public void RetryPolicy_FromOptions_RetryCountZero_UsesDefault3() - { - var options = new DrivelutionOptions - { - DefaultRetryCount = 0, - DefaultRetryIntervalSeconds = 5 - }; - - var policy = RetryPolicy.FromOptions(options); - - Assert.Equal(3, policy.MaxRetries); - } - - [Fact(DisplayName = "RetryPolicy_FromOptions_RetryCount为负数使用默认3")] - public void RetryPolicy_FromOptions_RetryCountNegative_UsesDefault3() - { - var options = new DrivelutionOptions - { - DefaultRetryCount = -5, - DefaultRetryIntervalSeconds = 5 - }; - - var policy = RetryPolicy.FromOptions(options); - - Assert.Equal(3, policy.MaxRetries); - } - - [Fact(DisplayName = "RetryPolicy_FromOptions_RetryInterval为0使用默认5")] - public void RetryPolicy_FromOptions_RetryIntervalZero_UsesDefault5() - { - var options = new DrivelutionOptions - { - DefaultRetryCount = 3, - DefaultRetryIntervalSeconds = 0 - }; - - var policy = RetryPolicy.FromOptions(options); - - Assert.Equal(TimeSpan.FromSeconds(5), policy.Delay); - } - - [Fact(DisplayName = "RetryPolicy_FromOptions_RetryInterval为负数使用默认5")] - public void RetryPolicy_FromOptions_RetryIntervalNegative_UsesDefault5() - { - var options = new DrivelutionOptions - { - DefaultRetryCount = 3, - DefaultRetryIntervalSeconds = -2 - }; - - var policy = RetryPolicy.FromOptions(options); - - Assert.Equal(TimeSpan.FromSeconds(5), policy.Delay); - } - - [Fact(DisplayName = "RetryPolicy_FromOptions_所有值为0使用默认值")] - public void RetryPolicy_FromOptions_AllZero_UsesDefaults() - { - var options = new DrivelutionOptions - { - DefaultRetryCount = 0, - DefaultRetryIntervalSeconds = 0 - }; - - var policy = RetryPolicy.FromOptions(options); - - Assert.Equal(3, policy.MaxRetries); - Assert.Equal(TimeSpan.FromSeconds(5), policy.Delay); - Assert.False(policy.UseExponentialBackoff); - } - - [Fact(DisplayName = "RetryPolicy_FromOptions_使用指数退避")] - public void RetryPolicy_FromOptions_UseExponentialBackoff() - { - var options = new DrivelutionOptions - { - DefaultRetryCount = 5, - DefaultRetryIntervalSeconds = 2, - UseExponentialBackoff = true - }; - - var policy = RetryPolicy.FromOptions(options); - - Assert.Equal(5, policy.MaxRetries); - Assert.Equal(TimeSpan.FromSeconds(2), policy.Delay); - Assert.True(policy.UseExponentialBackoff); - } - - [Fact(DisplayName = "RetryPolicy_ExecuteAsync_成功操作立即返回结果")] - public async Task RetryPolicy_ExecuteAsync_SuccessfulOperation_ReturnsImmediately() - { - var policy = new RetryPolicy(3, TimeSpan.FromMilliseconds(10)); - int callCount = 0; - - var result = await policy.ExecuteAsync(_ => - { - callCount++; - return Task.FromResult(42); - }); - - Assert.Equal(42, result); - Assert.Equal(1, callCount); - } - - [Fact(DisplayName = "RetryPolicy_ExecuteAsync_失败后重试成功")] - public async Task RetryPolicy_ExecuteAsync_RetriesAfterFailure() - { - var policy = new RetryPolicy(3, TimeSpan.FromMilliseconds(10)); - int callCount = 0; - - var result = await policy.ExecuteAsync(_ => - { - callCount++; - if (callCount < 3) - throw new InvalidOperationException("transient"); - return Task.FromResult("success"); - }); - - Assert.Equal("success", result); - Assert.Equal(3, callCount); - } - - [Fact(DisplayName = "RetryPolicy_ExecuteAsync_超过重试次数仍抛出最后异常")] - public async Task RetryPolicy_ExecuteAsync_ExceedsMaxRetries_Rethrows() - { - var policy = new RetryPolicy(2, TimeSpan.FromMilliseconds(5)); - int callCount = 0; - - var ex = await Assert.ThrowsAsync(() => - policy.ExecuteAsync(_ => - { - callCount++; - throw new InvalidOperationException($"fail {callCount}"); - }) - ); - - Assert.Contains("fail 3", ex.Message); // 1st call + 2 retries = 3 total - Assert.Equal(3, callCount); - } - - [Fact(DisplayName = "RetryPolicy_ExecuteAsync_OperationCanceledException不重试直接抛出")] - public async Task RetryPolicy_ExecuteAsync_Cancellation_ThrowsImmediately() - { - var policy = new RetryPolicy(5, TimeSpan.FromMilliseconds(10)); - int callCount = 0; - - await Assert.ThrowsAsync(() => - policy.ExecuteAsync(_ => - { - callCount++; - throw new OperationCanceledException("cancelled"); - }) - ); - - Assert.Equal(1, callCount); - } - - [Fact(DisplayName = "RetryPolicy_ExecuteAsync_MaxRetries为0不重试")] - public async Task RetryPolicy_ExecuteAsync_ZeroMaxRetries_NoRetry() - { - var policy = RetryPolicy.NoRetry; - int callCount = 0; - - await Assert.ThrowsAsync(() => - policy.ExecuteAsync(_ => - { - callCount++; - throw new InvalidOperationException("fail"); - }) - ); - - Assert.Equal(1, callCount); - } - - [Fact(DisplayName = "RetryPolicy_ExecuteWithRetryAsync_成功返回true")] - public async Task RetryPolicy_ExecuteWithRetryAsync_Success_ReturnsTrue() - { - var policy = new RetryPolicy(3, TimeSpan.FromMilliseconds(5)); - var result = await policy.ExecuteWithRetryAsync(_ => Task.FromResult(true)); - Assert.True(result); - } - - [Fact(DisplayName = "RetryPolicy_ExecuteWithRetryAsync_最终失败返回false")] - public async Task RetryPolicy_ExecuteWithRetryAsync_AlwaysFalse_ReturnsFalse() - { - var policy = new RetryPolicy(2, TimeSpan.FromMilliseconds(5)); - int callCount = 0; - - var result = await policy.ExecuteWithRetryAsync(_ => - { - callCount++; - return Task.FromResult(false); - }); - - Assert.False(result); - Assert.Equal(3, callCount); // 1 + 2 retries - } - - [Fact(DisplayName = "RetryPolicy_ExecuteWithRetryAsync_重试后成功返回true")] - public async Task RetryPolicy_ExecuteWithRetryAsync_RetrySuccess_ReturnsTrue() - { - var policy = new RetryPolicy(3, TimeSpan.FromMilliseconds(5)); - int callCount = 0; - - var result = await policy.ExecuteWithRetryAsync(_ => - { - callCount++; - if (callCount < 3) - throw new Exception("transient"); - return Task.FromResult(true); - }); - - Assert.True(result); - Assert.Equal(3, callCount); - } - - [Fact(DisplayName = "RetryPolicy_ExecuteWithRetryAsync_OperationCanceledException不重试")] - public async Task RetryPolicy_ExecuteWithRetryAsync_Cancellation_Rethrows() - { - var policy = new RetryPolicy(5, TimeSpan.FromMilliseconds(5)); - int callCount = 0; - - await Assert.ThrowsAsync(() => - policy.ExecuteWithRetryAsync(_ => - { - callCount++; - throw new OperationCanceledException(); - }) - ); - - Assert.Equal(1, callCount); - } - - [Fact(DisplayName = "RetryPolicy_使用CancellationToken取消时立即停止")] - public async Task RetryPolicy_WithCancellationToken_CancelsImmediately() - { - var policy = new RetryPolicy(10, TimeSpan.FromMilliseconds(100)); - using var cts = new CancellationTokenSource(); - - int callCount = 0; - var task = policy.ExecuteAsync(ct => - { - callCount++; - cts.Cancel(); - throw new InvalidOperationException("fail"); - }, cts.Token); - - await Assert.ThrowsAsync(() => task); - Assert.Equal(1, callCount); - } -} diff --git a/src/c#/DrivelutionTest/Utilities/CompatibilityCheckerTests.cs b/src/c#/DrivelutionTest/Utilities/CompatibilityCheckerTests.cs deleted file mode 100644 index a760f44f..00000000 --- a/src/c#/DrivelutionTest/Utilities/CompatibilityCheckerTests.cs +++ /dev/null @@ -1,230 +0,0 @@ -using GeneralUpdate.Drivelution.Abstractions.Models; -using GeneralUpdate.Drivelution.Core.Utilities; - -namespace DrivelutionTest.Utilities; - -/// -/// CompatibilityChecker 测试 -/// 分支覆盖点: -/// - CheckCompatibility: null DriverInfo -> ArgumentNullException -/// - OS兼容检查: 匹配->true, 不匹配->false -/// - 架构兼容检查: 匹配->true, 不匹配->false -/// - TargetOS为空/null -> 假设兼容 (true) -/// - Architecture为空/null -> 假设兼容 (true) -/// - GetCurrentOS: Windows/Linux/MacOS/Unknown -/// - GetCurrentArchitecture -/// - GetSystemVersion -/// - GetCompatibilityReport: 完整报告 -/// - NormalizeArchitecture: X64/AMD64/X86_64 -> X64, X86/I386/I686 -> X86, ARM64/AARCH64 -> ARM64, ARM/ARMV7 -> ARM -/// - CheckCompatibilityAsync -/// 触发条件:创建 DriverInfo 实例 -/// 预期结果:兼容性检查正确 -/// -public class CompatibilityCheckerTests -{ - [Fact(DisplayName = "CompatibilityChecker_CheckCompatibility_null参数抛出ArgumentNullException")] - public async Task CheckCompatibility_NullDriverInfo_ThrowsArgumentNullException() - { - await Assert.ThrowsAsync(() => - CompatibilityChecker.CheckCompatibilityAsync(null!)); - } - - [Fact(DisplayName = "CompatibilityChecker_GetCurrentOS_返回非空字符串")] - public void GetCurrentOS_ReturnsNonNullString() - { - var os = CompatibilityChecker.GetCurrentOS(); - Assert.NotNull(os); - Assert.NotEmpty(os); - } - - [Fact(DisplayName = "CompatibilityChecker_GetCurrentArchitecture_返回非空字符串")] - public void GetCurrentArchitecture_ReturnsNonNullString() - { - var arch = CompatibilityChecker.GetCurrentArchitecture(); - Assert.NotNull(arch); - Assert.NotEmpty(arch); - } - - [Fact(DisplayName = "CompatibilityChecker_GetSystemVersion_返回非空字符串")] - public void GetSystemVersion_ReturnsNonNullString() - { - var version = CompatibilityChecker.GetSystemVersion(); - Assert.NotNull(version); - Assert.NotEmpty(version); - } - - [Fact(DisplayName = "CompatibilityChecker_GetCompatibilityReport_包含所有字段")] - public void GetCompatibilityReport_ContainsAllFields() - { - var driverInfo = new DriverInfo - { - TargetOS = "Windows", - Architecture = "x64" - }; - - var report = CompatibilityChecker.GetCompatibilityReport(driverInfo); - - Assert.Equal("Windows", report.TargetOS); - Assert.Equal("x64", report.TargetArchitecture); - Assert.NotNull(report.CurrentOS); - Assert.NotNull(report.CurrentArchitecture); - Assert.NotNull(report.SystemVersion); - Assert.Equal(report.OSCompatible && report.ArchitectureCompatible, report.OverallCompatible); - } - - [Fact(DisplayName = "CompatibilityChecker_CheckCompatibility_TargetOS为空假定兼容")] - public void CheckCompatibility_EmptyTargetOS_AssumesCompatible() - { - var driverInfo = new DriverInfo - { - TargetOS = "", - Architecture = "" - }; - - var result = CompatibilityChecker.CheckCompatibility(driverInfo); - - Assert.True(result); - } - - [Fact(DisplayName = "CompatibilityChecker_CheckCompatibility_TargetOS为空格假定兼容")] - public void CheckCompatibility_WhitespaceTargetOS_AssumesCompatible() - { - var driverInfo = new DriverInfo - { - TargetOS = " ", - Architecture = "" - }; - - var result = CompatibilityChecker.CheckCompatibility(driverInfo); - - Assert.True(result); - } - - [Fact(DisplayName = "CompatibilityChecker_GetCompatibilityReport_TargetOS为空时OverallCompatible为true")] - public void GetCompatibilityReport_EmptyTargetOS_OverallCompatibleTrue() - { - var driverInfo = new DriverInfo - { - TargetOS = "", - Architecture = "" - }; - - var report = CompatibilityChecker.GetCompatibilityReport(driverInfo); - - Assert.True(report.OverallCompatible); - Assert.True(report.OSCompatible); - Assert.True(report.ArchitectureCompatible); - } - - [Fact(DisplayName = "CompatibilityChecker_CheckCompatibility_不匹配OS返回false")] - public void CheckCompatibility_MismatchedOS_ReturnsFalse() - { - var driverInfo = new DriverInfo - { - TargetOS = "FreeBSD", - Architecture = "" - }; - - var result = CompatibilityChecker.CheckCompatibility(driverInfo); - - Assert.False(result); - } - - [Fact(DisplayName = "CompatibilityChecker_NormalizeArchitecture_X64别名正常化")] - public void GetCompatibilityReport_NormalizeArchitecture_X64Aliases() - { - // Architecture normalization is handled internally in IsArchitectureCompatible - // We verify via the CompatibilityReport that normalization works - var currentArch = CompatibilityChecker.GetCurrentArchitecture(); - Assert.NotNull(currentArch); - } - - [Fact(DisplayName = "CompatibilityChecker_CheckCompatibilityAsync_异步返回结果")] - public async Task CheckCompatibilityAsync_ReturnsCompatibilityResult() - { - var driverInfo = new DriverInfo - { - TargetOS = "", - Architecture = "" - }; - - var result = await CompatibilityChecker.CheckCompatibilityAsync(driverInfo); - - Assert.True(result); - } -} - -/// -/// CompatibilityReport 测试 -/// 分支覆盖点: -/// - 默认构造函数属性默认值 -/// - 属性可读写 -/// - OverallCompatible = OSCompatible && ArchitectureCompatible -/// 触发条件:创建 CompatibilityReport -/// 预期结果:属性正确链接 -/// -public class CompatibilityReportTests -{ - [Fact(DisplayName = "CompatibilityReport_默认构造函数_所有属性为默认值")] - public void CompatibilityReport_DefaultConstructor_AllPropertiesHaveDefaultValues() - { - var report = new CompatibilityReport(); - - Assert.Equal(string.Empty, report.CurrentOS); - Assert.Equal(string.Empty, report.CurrentArchitecture); - Assert.Equal(string.Empty, report.SystemVersion); - Assert.Equal(string.Empty, report.TargetOS); - Assert.Equal(string.Empty, report.TargetArchitecture); - Assert.False(report.OSCompatible); - Assert.False(report.ArchitectureCompatible); - Assert.False(report.OverallCompatible); - } - - [Fact(DisplayName = "CompatibilityReport_OverallCompatible_OS和架构都兼容时返回true")] - public void CompatibilityReport_OverallCompatible_BothCompatible_ReturnsTrue() - { - var report = new CompatibilityReport - { - OSCompatible = true, - ArchitectureCompatible = true - }; - - Assert.True(report.OverallCompatible); - } - - [Fact(DisplayName = "CompatibilityReport_OverallCompatible_仅OS兼容时返回false")] - public void CompatibilityReport_OverallCompatible_OnlyOSCompatible_ReturnsFalse() - { - var report = new CompatibilityReport - { - OSCompatible = true, - ArchitectureCompatible = false - }; - - Assert.False(report.OverallCompatible); - } - - [Fact(DisplayName = "CompatibilityReport_OverallCompatible_仅架构兼容时返回false")] - public void CompatibilityReport_OverallCompatible_OnlyArchCompatible_ReturnsFalse() - { - var report = new CompatibilityReport - { - OSCompatible = false, - ArchitectureCompatible = true - }; - - Assert.False(report.OverallCompatible); - } - - [Fact(DisplayName = "CompatibilityReport_OverallCompatible_都false时返回false")] - public void CompatibilityReport_OverallCompatible_NeitherCompatible_ReturnsFalse() - { - var report = new CompatibilityReport - { - OSCompatible = false, - ArchitectureCompatible = false - }; - - Assert.False(report.OverallCompatible); - } -} diff --git a/src/c#/DrivelutionTest/Utilities/HashValidatorTests.cs b/src/c#/DrivelutionTest/Utilities/HashValidatorTests.cs deleted file mode 100644 index 197371f6..00000000 --- a/src/c#/DrivelutionTest/Utilities/HashValidatorTests.cs +++ /dev/null @@ -1,210 +0,0 @@ -using GeneralUpdate.Drivelution.Core.Utilities; - -namespace DrivelutionTest.Utilities; - -/// -/// HashValidator 测试 -/// 分支覆盖点: -/// - ComputeHashAsync: 文件存在且有效 -> 返回hash; 文件不存在 -> FileNotFoundException -/// - 算法: SHA256 (默认), MD5 -/// - 不支持的算法 -> ArgumentException -/// - ValidateHashAsync: null/空 expectedHash -> ArgumentException -/// - ValidateHashAsync: 匹配 -> true, 不匹配 -> false -/// - ComputeStringHash: 空/空字符串 input -> ArgumentException -/// - ComputeStringHash: SHA256, MD5, 不支持的算法 -/// - ByteArrayToHexString 内部实现 -/// 触发条件:创建临时文件或字符串来测试 -/// 预期结果:哈希正确,异常正确 -/// -public class HashValidatorTests : IDisposable -{ - private string _tempFilePath; - - public HashValidatorTests() - { - _tempFilePath = Path.GetTempFileName(); - } - - public void Dispose() - { - if (File.Exists(_tempFilePath)) - File.Delete(_tempFilePath); - } - - [Fact(DisplayName = "HashValidator_ComputeHashAsync_SHA256返回64字符hex")] - public async Task ComputeHashAsync_SHA256_Returns64CharHex() - { - await File.WriteAllTextAsync(_tempFilePath, "test data"); - - var hash = await HashValidator.ComputeHashAsync(_tempFilePath); - - Assert.NotNull(hash); - Assert.Equal(64, hash.Length); - } - - [Fact(DisplayName = "HashValidator_ComputeHashAsync_MD5返回32字符hex")] - public async Task ComputeHashAsync_MD5_Returns32CharHex() - { - await File.WriteAllTextAsync(_tempFilePath, "test data"); - - var hash = await HashValidator.ComputeHashAsync(_tempFilePath, "MD5"); - - Assert.NotNull(hash); - Assert.Equal(32, hash.Length); - } - - [Fact(DisplayName = "HashValidator_ComputeHashAsync_文件不存在抛出FileNotFoundException")] - public async Task ComputeHashAsync_FileNotFound_ThrowsFileNotFoundException() - { - await Assert.ThrowsAsync(() => - HashValidator.ComputeHashAsync("nonexistent_file.xyz")); - } - - [Fact(DisplayName = "HashValidator_ComputeHashAsync_不支持的算法抛出ArgumentException")] - public async Task ComputeHashAsync_UnsupportedAlgorithm_ThrowsArgumentException() - { - await File.WriteAllTextAsync(_tempFilePath, "data"); - - await Assert.ThrowsAsync(() => - HashValidator.ComputeHashAsync(_tempFilePath, "SHA1")); - } - - [Fact(DisplayName = "HashValidator_ComputeHashAsync_大小写不敏感算法名")] - public async Task ComputeHashAsync_CaseInsensitiveAlgorithm_Works() - { - await File.WriteAllTextAsync(_tempFilePath, "data"); - - var hash = await HashValidator.ComputeHashAsync(_tempFilePath, "sha256"); - - Assert.NotNull(hash); - Assert.Equal(64, hash.Length); - } - - [Fact(DisplayName = "HashValidator_ComputeHashAsync_相同内容生成相同哈希")] - public async Task ComputeHashAsync_SameContent_SameHash() - { - await File.WriteAllTextAsync(_tempFilePath, "identical content"); - - var hash1 = await HashValidator.ComputeHashAsync(_tempFilePath); - var hash2 = await HashValidator.ComputeHashAsync(_tempFilePath); - - Assert.Equal(hash1, hash2); - } - - [Fact(DisplayName = "HashValidator_ComputeHashAsync_不同内容生成不同哈希")] - public async Task ComputeHashAsync_DifferentContent_DifferentHash() - { - await File.WriteAllTextAsync(_tempFilePath, "content A"); - var hash1 = await HashValidator.ComputeHashAsync(_tempFilePath); - - await File.WriteAllTextAsync(_tempFilePath, "content B"); - var hash2 = await HashValidator.ComputeHashAsync(_tempFilePath); - - Assert.NotEqual(hash1, hash2); - } - - [Fact(DisplayName = "HashValidator_ValidateHashAsync_匹配返回true")] - public async Task ValidateHashAsync_MatchingHash_ReturnsTrue() - { - await File.WriteAllTextAsync(_tempFilePath, "hello world"); - - var hash = await HashValidator.ComputeHashAsync(_tempFilePath); - - Assert.True(await HashValidator.ValidateHashAsync(_tempFilePath, hash)); - } - - [Fact(DisplayName = "HashValidator_ValidateHashAsync_不匹配返回false")] - public async Task ValidateHashAsync_MismatchingHash_ReturnsFalse() - { - await File.WriteAllTextAsync(_tempFilePath, "hello world"); - - Assert.False(await HashValidator.ValidateHashAsync(_tempFilePath, - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")); - } - - [Theory(DisplayName = "HashValidator_ValidateHashAsync_null或空expectedHash抛出ArgumentException")] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - public async Task ValidateHashAsync_NullOrEmptyExpectedHash_ThrowsArgumentException(string? expectedHash) - { - await File.WriteAllTextAsync(_tempFilePath, "data"); - - await Assert.ThrowsAsync(() => - HashValidator.ValidateHashAsync(_tempFilePath, expectedHash!)); - } - - [Fact(DisplayName = "HashValidator_ComputeStringHash_SHA256返回结果")] - public void ComputeStringHash_SHA256_ReturnsHash() - { - var hash = HashValidator.ComputeStringHash("test input"); - - Assert.NotNull(hash); - Assert.Equal(64, hash.Length); - } - - [Fact(DisplayName = "HashValidator_ComputeStringHash_MD5返回结果")] - public void ComputeStringHash_MD5_ReturnsHash() - { - var hash = HashValidator.ComputeStringHash("test input", "MD5"); - - Assert.NotNull(hash); - Assert.Equal(32, hash.Length); - } - - [Theory(DisplayName = "HashValidator_ComputeStringHash_null或空输入抛出ArgumentException")] - [InlineData(null)] - [InlineData("")] - public void ComputeStringHash_NullOrEmptyInput_ThrowsArgumentException(string? input) - { - Assert.Throws(() => HashValidator.ComputeStringHash(input!)); - } - - [Fact(DisplayName = "HashValidator_ComputeStringHash_不支持的算法抛出ArgumentException")] - public void ComputeStringHash_UnsupportedAlgorithm_ThrowsArgumentException() - { - Assert.Throws(() => HashValidator.ComputeStringHash("data", "SHA1")); - } - - [Fact(DisplayName = "HashValidator_ComputeStringHash_相同输入产生相同哈希")] - public void ComputeStringHash_SameInput_SameHash() - { - var h1 = HashValidator.ComputeStringHash("hello"); - var h2 = HashValidator.ComputeStringHash("hello"); - - Assert.Equal(h1, h2); - } - - [Fact(DisplayName = "HashValidator_ComputeStringHash_不同输入产生不同哈希")] - public void ComputeStringHash_DifferentInput_DifferentHash() - { - var h1 = HashValidator.ComputeStringHash("hello"); - var h2 = HashValidator.ComputeStringHash("world"); - - Assert.NotEqual(h1, h2); - } - - [Fact(DisplayName = "HashValidator_ComputeHashAsync_空文件仍返回有效哈希")] - public async Task ComputeHashAsync_EmptyFile_ReturnsValidHash() - { - await File.WriteAllTextAsync(_tempFilePath, ""); - - var hash = await HashValidator.ComputeHashAsync(_tempFilePath); - - Assert.NotNull(hash); - Assert.Equal(64, hash.Length); - } - - [Fact(DisplayName = "HashValidator_ValidateHashAsync_大小写不敏感比较")] - public async Task ValidateHashAsync_CaseInsensitiveComparison_ReturnsTrue() - { - await File.WriteAllTextAsync(_tempFilePath, "data"); - - var hash = await HashValidator.ComputeHashAsync(_tempFilePath); - var upperHash = hash.ToUpper(); - var lowerHash = hash.ToLower(); - - Assert.True(await HashValidator.ValidateHashAsync(_tempFilePath, upperHash)); - Assert.True(await HashValidator.ValidateHashAsync(_tempFilePath, lowerHash)); - } -} diff --git a/src/c#/DrivelutionTest/Utilities/RestartHelperTests.cs b/src/c#/DrivelutionTest/Utilities/RestartHelperTests.cs deleted file mode 100644 index aa6baf8c..00000000 --- a/src/c#/DrivelutionTest/Utilities/RestartHelperTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -using GeneralUpdate.Drivelution.Core.Utilities; -using GeneralUpdate.Drivelution.Abstractions.Models; - -namespace DrivelutionTest.Utilities; - -/// -/// RestartHelper 测试 -/// 分支覆盖点: -/// - HandleRestartAsync: RestartMode.None -> true -/// - HandleRestartAsync: RestartMode.Prompt -> PromptUserForRestart (返回false) -/// - HandleRestartAsync: RestartMode.Delayed -> 延迟后调用重启 -/// - HandleRestartAsync: RestartMode.Immediate -> 立即重启 -/// - HandleRestartAsync: 未知RestartMode -> false -/// - PromptUserForRestart: 始终返回false (简化实现) -/// - IsRestartRequired: None -> false, 其他 -> true -/// - RestartCurrentProcess: 会导致进程退出(无法直接测试),但可验证调用不抛异常 -/// - RestartSystemAsync: 端到端测试(会尝试真实重启, 此处只测试异常处理路径) -/// 触发条件:调用各辅助方法 -/// 预期结果:模式分发正确 -/// -public class RestartHelperTests -{ - [Fact(DisplayName = "RestartHelper_HandleRestartAsync_RestartModeNone返回true")] - public async Task HandleRestartAsync_ModeNone_ReturnsTrue() - { - var result = await RestartHelper.HandleRestartAsync(RestartMode.None); - - Assert.True(result); - } - - [Fact(DisplayName = "RestartHelper_HandleRestartAsync_RestartModePrompt返回false")] - public async Task HandleRestartAsync_ModePrompt_ReturnsFalse() - { - // PromptUserForRestart always returns false in simplified implementation - var result = await RestartHelper.HandleRestartAsync(RestartMode.Prompt); - - Assert.False(result); - } - - [Fact(DisplayName = "RestartHelper_PromptUserForRestart_返回false")] - public void PromptUserForRestart_ReturnsFalse() - { - var result = RestartHelper.PromptUserForRestart(); - - Assert.False(result); - } - - [Fact(DisplayName = "RestartHelper_PromptUserForRestart_带消息参数返回false")] - public void PromptUserForRestart_WithMessage_ReturnsFalse() - { - var result = RestartHelper.PromptUserForRestart("Custom message"); - - Assert.False(result); - } - - [Fact(DisplayName = "RestartHelper_PromptUserForRestart_空消息使用默认消息")] - public void PromptUserForRestart_EmptyMessage_UsesDefault() - { - var result = RestartHelper.PromptUserForRestart(""); - - Assert.False(result); - } - - [Fact(DisplayName = "RestartHelper_IsRestartRequired_None模式返回false")] - public void IsRestartRequired_ModeNone_ReturnsFalse() - { - Assert.False(RestartHelper.IsRestartRequired(RestartMode.None)); - } - - [Theory(DisplayName = "RestartHelper_IsRestartRequired_非None模式返回true")] - [InlineData(RestartMode.Prompt)] - [InlineData(RestartMode.Delayed)] - [InlineData(RestartMode.Immediate)] - public void IsRestartRequired_NonNoneMode_ReturnsTrue(RestartMode mode) - { - Assert.True(RestartHelper.IsRestartRequired(mode)); - } - - [Fact(DisplayName = "RestartHelper_HandleRestartAsync_Delayed模式_等待后尝试重启")] - public async Task HandleRestartAsync_ModeDelayed_WaitsAndReturns() - { - using var cts = new CancellationTokenSource(2000); - - var task = RestartHelper.HandleRestartAsync(RestartMode.Delayed, 1, "test"); - - // Should complete quickly (1 second delay) or fail fast on non-elevated - try { await task.WaitAsync(cts.Token); } catch (OperationCanceledException) { } - } - - [Fact(DisplayName = "RestartHelper_HandleRestartAsync_Immediate模式返回结果")] - public async Task HandleRestartAsync_ModeImmediate_ReturnsResult() - { - using var cts = new CancellationTokenSource(2000); - - var task = RestartHelper.HandleRestartAsync(RestartMode.Immediate); - - try { await task.WaitAsync(cts.Token); } catch (OperationCanceledException) { } - } - - [Fact(DisplayName = "RestartHelper_HandleRestartAsync_未知模式返回false")] - public async Task HandleRestartAsync_UnknownMode_ReturnsFalse() - { - var result = await RestartHelper.HandleRestartAsync((RestartMode)999); - - Assert.False(result); - } -} diff --git a/src/c#/DrivelutionTest/Utilities/VersionComparerTests.cs b/src/c#/DrivelutionTest/Utilities/VersionComparerTests.cs deleted file mode 100644 index 4bcbc6dd..00000000 --- a/src/c#/DrivelutionTest/Utilities/VersionComparerTests.cs +++ /dev/null @@ -1,178 +0,0 @@ -using GeneralUpdate.Drivelution.Core.Utilities; - -namespace DrivelutionTest.Utilities; - -/// -/// VersionComparer 测试 -/// 分支覆盖点: -/// - Compare: 正常版本比较, null/空字符串抛出 ArgumentException -/// - Major/Minor/Patch 不同时的比较 -/// - 预发布版本比较: 正式版 > 预发布版 -/// - 预发布标识符比较: 数字 vs 数字, 字母 vs 字母, 数字 vs 字母 -/// - 较长预发布 > 较短预发布 -/// - IsGreaterThan / IsLessThan / IsEqual 辅助方法 -/// - IsValidSemVer: 有效版本返回 true, null/空/无效返回 false -/// - 非SemVer格式抛出 FormatException -/// 触发条件:调用各比较和验证方法 -/// 预期结果:正确比较和验证 -/// -public class VersionComparerTests -{ - [Theory(DisplayName = "VersionComparer_Compare_相同版本返回0")] - [InlineData("1.0.0", "1.0.0")] - [InlineData("2.1.3", "2.1.3")] - [InlineData("0.0.0", "0.0.0")] - [InlineData("10.20.30", "10.20.30")] - public void Compare_SameVersions_ReturnsZero(string v1, string v2) - { - Assert.Equal(0, VersionComparer.Compare(v1, v2)); - } - - [Theory(DisplayName = "VersionComparer_Compare_v1大于v2返回1")] - [InlineData("2.0.0", "1.0.0")] - [InlineData("1.2.0", "1.1.0")] - [InlineData("1.0.3", "1.0.2")] - [InlineData("10.0.0", "9.99.99")] - public void Compare_V1Greater_ReturnsOne(string v1, string v2) - { - Assert.Equal(1, VersionComparer.Compare(v1, v2)); - } - - [Theory(DisplayName = "VersionComparer_Compare_v1小于v2返回-1")] - [InlineData("1.0.0", "2.0.0")] - [InlineData("1.1.0", "1.2.0")] - [InlineData("1.0.2", "1.0.3")] - public void Compare_V1Less_ReturnsMinusOne(string v1, string v2) - { - Assert.Equal(-1, VersionComparer.Compare(v1, v2)); - } - - [Theory(DisplayName = "VersionComparer_Compare_正式版大于预发布版")] - [InlineData("1.0.0", "1.0.0-alpha")] - [InlineData("1.0.0", "1.0.0-beta.1")] - public void Compare_ReleaseGreaterThanPreRelease_ReturnsOne(string v1, string v2) - { - Assert.True(VersionComparer.Compare(v1, v2) > 0); - } - - [Theory(DisplayName = "VersionComparer_Compare_预发布版小于正式版")] - [InlineData("1.0.0-alpha", "1.0.0")] - [InlineData("1.0.0-rc.1", "1.0.0")] - public void Compare_PreReleaseLessThanRelease_ReturnsMinusOne(string v1, string v2) - { - Assert.True(VersionComparer.Compare(v1, v2) < 0); - } - - [Theory(DisplayName = "VersionComparer_Compare_预发布版本号比较")] - [InlineData("1.0.0-1", "1.0.0-2")] - [InlineData("1.0.0-alpha", "1.0.0-beta")] - [InlineData("1.0.0-alpha.1", "1.0.0-alpha.2")] - public void Compare_PreReleaseComparison_OrderedCorrectly(string v1, string v2) - { - Assert.True(VersionComparer.Compare(v1, v2) < 0); - } - - [Fact(DisplayName = "VersionComparer_Compare_数字预发布标识小于字母标识")] - public void Compare_NumericPreReleaseLessThanAlpha() - { - Assert.True(VersionComparer.Compare("1.0.0-1", "1.0.0-alpha") < 0); - } - - [Fact(DisplayName = "VersionComparer_Compare_较长预发布标识更大")] - public void Compare_LongerPreReleaseIsGreater() - { - Assert.True(VersionComparer.Compare("1.0.0-alpha.1.2", "1.0.0-alpha.1") > 0); - } - - [Theory(DisplayName = "VersionComparer_Compare_null或空字符串抛出ArgumentException")] - [InlineData(null, "1.0.0")] - [InlineData("1.0.0", null)] - [InlineData("", "1.0.0")] - [InlineData("1.0.0", "")] - [InlineData(" ", "1.0.0")] - public void Compare_NullOrEmpty_ThrowsArgumentException(string? v1, string? v2) - { - Assert.Throws(() => VersionComparer.Compare(v1!, v2!)); - } - - [Fact(DisplayName = "VersionComparer_Compare_无效版本格式抛出FormatException")] - public void Compare_InvalidFormat_ThrowsFormatException() - { - Assert.Throws(() => VersionComparer.Compare("not.a.version", "1.0.0")); - } - - [Theory(DisplayName = "VersionComparer_IsGreaterThan_正确判断")] - [InlineData("2.0.0", "1.0.0", true)] - [InlineData("1.0.0", "2.0.0", false)] - [InlineData("1.0.0", "1.0.0", false)] - public void IsGreaterThan_CorrectlyDetermined(string v1, string v2, bool expected) - { - Assert.Equal(expected, VersionComparer.IsGreaterThan(v1, v2)); - } - - [Theory(DisplayName = "VersionComparer_IsLessThan_正确判断")] - [InlineData("1.0.0", "2.0.0", true)] - [InlineData("2.0.0", "1.0.0", false)] - [InlineData("1.0.0", "1.0.0", false)] - public void IsLessThan_CorrectlyDetermined(string v1, string v2, bool expected) - { - Assert.Equal(expected, VersionComparer.IsLessThan(v1, v2)); - } - - [Theory(DisplayName = "VersionComparer_IsEqual_正确判断")] - [InlineData("1.0.0", "1.0.0", true)] - [InlineData("1.0.0", "1.0.1", false)] - public void IsEqual_CorrectlyDetermined(string v1, string v2, bool expected) - { - Assert.Equal(expected, VersionComparer.IsEqual(v1, v2)); - } - - [Theory(DisplayName = "VersionComparer_IsValidSemVer_有效版本格式")] - [InlineData("1.0.0")] - [InlineData("0.0.0")] - [InlineData("10.20.30")] - [InlineData("1.0.0-alpha")] - [InlineData("1.0.0-alpha.1")] - [InlineData("1.0.0-alpha.beta")] - [InlineData("1.0.0+build")] - [InlineData("1.0.0-alpha+build")] - [InlineData("1.0.0+build.123")] - public void IsValidSemVer_ValidVersions_ReturnsTrue(string version) - { - Assert.True(VersionComparer.IsValidSemVer(version)); - } - - [Theory(DisplayName = "VersionComparer_IsValidSemVer_无效版本格式")] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - [InlineData("1")] - [InlineData("1.0")] - [InlineData("v1.0.0")] - [InlineData("1.0.0.0")] - [InlineData("not.a.version")] - public void IsValidSemVer_InvalidVersions_ReturnsFalse(string? version) - { - Assert.False(VersionComparer.IsValidSemVer(version)); - } - - [Theory(DisplayName = "VersionComparer_Compare_带BuildMetadata版本正确比较")] - [InlineData("1.0.0+build1", "1.0.0+build2")] - public void Compare_WithBuildMetadata_ComparedCorrectly(string v1, string v2) - { - Assert.Equal(0, VersionComparer.Compare(v1, v2)); - } - - [Theory(DisplayName = "VersionComparer_Compare_包含预发布和构建元数据")] - [InlineData("1.0.0-alpha.1+build", "1.0.0-alpha.2+build")] - public void Compare_WithBuildAndPreRelease_CorrectOrder(string v1, string v2) - { - Assert.True(VersionComparer.Compare(v1, v2) < 0); - } - - [Fact(DisplayName = "VersionComparer_Compare_Major值极大版本比较")] - public void Compare_VeryLargeMajorVersion_Works() - { - Assert.Equal(1, VersionComparer.Compare("999999.0.0", "999998.0.0")); - } -} diff --git a/src/c#/DrivelutionTest/WindowsImplementations/WindowsDriverBackupTests.cs b/src/c#/DrivelutionTest/WindowsImplementations/WindowsDriverBackupTests.cs deleted file mode 100644 index e79179e1..00000000 --- a/src/c#/DrivelutionTest/WindowsImplementations/WindowsDriverBackupTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -using GeneralUpdate.Drivelution.Windows.Implementation; -using GeneralUpdate.Drivelution.Abstractions.Exceptions; - -namespace DrivelutionTest.WindowsImplementations; - -/// -/// WindowsDriverBackup 测试 -/// 分支覆盖点: -/// - BackupAsync: 源文件不存在 -> FileNotFoundException -/// - BackupAsync: 备份目录不存在 -> 自动创建 -/// - BackupAsync: 成功备份 -> true -/// - BackupAsync: 异常 -> DriverBackupException -/// - RestoreAsync: 备份文件不存在 -> FileNotFoundException -/// - RestoreAsync: 目标文件已存在 -> 重命名为.old -/// - RestoreAsync: 成功恢复 -> true -/// - RestoreAsync: 异常 -> DriverRollbackException -/// - DeleteBackupAsync: 文件存在 -> 删除返回true -/// - DeleteBackupAsync: 文件不存在 -> 返回false -/// - DeleteBackupAsync: 异常 -> 返回false -/// 触发条件:创建临时文件测试备份/恢复/删除 -/// 预期结果:I/O操作正确执行 -/// -public class WindowsDriverBackupTests : IDisposable -{ - private readonly string _tempDir; - private readonly WindowsDriverBackup _backup; - - public WindowsDriverBackupTests() - { - _tempDir = Path.Combine(Path.GetTempPath(), $"drivelution_test_{Guid.NewGuid():N}"); - Directory.CreateDirectory(_tempDir); - _backup = new WindowsDriverBackup(); - } - - public void Dispose() - { - try { if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); } - catch { /* cleanup best-effort */ } - } - - [Fact(DisplayName = "WindowsDriverBackup_BackupAsync_源文件不存在_抛出FileNotFoundException")] - public async Task BackupAsync_SourceNotExists_ThrowsFileNotFoundException() - { - await Assert.ThrowsAsync(() => - _backup.BackupAsync(Path.Combine(_tempDir, "nonexistent.sys"), - Path.Combine(_tempDir, "backup"))); - } - - [Fact(DisplayName = "WindowsDriverBackup_BackupAsync_成功备份_返回true")] - public async Task BackupAsync_SuccessfulBackup_ReturnsTrue() - { - var sourceFile = Path.Combine(_tempDir, "test.sys"); - await File.WriteAllTextAsync(sourceFile, "driver data"); - var backupPath = Path.Combine(_tempDir, "backups", "driver_backup"); - - var result = await _backup.BackupAsync(sourceFile, backupPath); - - Assert.True(result); - // Directory should be created - Assert.True(Directory.Exists(Path.Combine(_tempDir, "backups"))); - } - - [Fact(DisplayName = "WindowsDriverBackup_RestoreAsync_备份文件不存在_抛出FileNotFoundException")] - public async Task RestoreAsync_BackupNotExists_ThrowsFileNotFoundException() - { - await Assert.ThrowsAsync(() => - _backup.RestoreAsync(Path.Combine(_tempDir, "nobackup.sys"), - Path.Combine(_tempDir, "target.sys"))); - } - - [Fact(DisplayName = "WindowsDriverBackup_RestoreAsync_成功恢复_返回true")] - public async Task RestoreAsync_SuccessfulRestore_ReturnsTrue() - { - // Create backup - var backupFile = Path.Combine(_tempDir, "backup_driver.sys"); - await File.WriteAllTextAsync(backupFile, "backup data"); - var targetFile = Path.Combine(_tempDir, "restored_driver.sys"); - - var result = await _backup.RestoreAsync(backupFile, targetFile); - - Assert.True(result); - Assert.True(File.Exists(targetFile)); - } - - [Fact(DisplayName = "WindowsDriverBackup_RestoreAsync_目标文件已存在_先重命名")] - public async Task RestoreAsync_TargetExists_RenamesFirst() - { - var backupFile = Path.Combine(_tempDir, "backup_driver2.sys"); - await File.WriteAllTextAsync(backupFile, "backup data"); - var targetFile = Path.Combine(_tempDir, "existing_target.sys"); - await File.WriteAllTextAsync(targetFile, "existing data"); - - var result = await _backup.RestoreAsync(backupFile, targetFile); - - Assert.True(result); - Assert.True(File.Exists(targetFile + ".old")); - } - - [Fact(DisplayName = "WindowsDriverBackup_DeleteBackupAsync_文件存在_返回true")] - public async Task DeleteBackupAsync_FileExists_ReturnsTrue() - { - var file = Path.Combine(_tempDir, "to_delete.sys"); - await File.WriteAllTextAsync(file, "data"); - - var result = await _backup.DeleteBackupAsync(file); - - Assert.True(result); - Assert.False(File.Exists(file)); - } - - [Fact(DisplayName = "WindowsDriverBackup_DeleteBackupAsync_文件不存在_返回false")] - public async Task DeleteBackupAsync_FileNotExists_ReturnsFalse() - { - var result = await _backup.DeleteBackupAsync(Path.Combine(_tempDir, "nonexistent.sys")); - Assert.False(result); - } -} diff --git a/src/c#/ExtensionTest/Communication/ExtensionHttpClientTests.cs b/src/c#/ExtensionTest/Communication/ExtensionHttpClientTests.cs deleted file mode 100644 index 19839a73..00000000 --- a/src/c#/ExtensionTest/Communication/ExtensionHttpClientTests.cs +++ /dev/null @@ -1,447 +0,0 @@ -/// -/// 测试覆盖点: -/// - 构造函数 -/// - (serverUrl, scheme, token) 便利构造函数 -/// - (serverUrl, scheme, token, httpClient, ownsHttpClient) 完整构造函数 -/// - serverUrl 为 null => ArgumentNullException -/// - httpClient 为 null => ArgumentNullException -/// - serverUrl 以 '/' 结尾 => 被 TrimEnd -/// - scheme/token 为空 => Authorization header 不设置 -/// - scheme/token 非空 => Authorization header 正确设置 -/// - QueryExtensionsAsync(query, ct) -/// - 成功响应 => 反序列化返回 DTO -/// - 非成功状态码 => 返回 Message 含 HTTP 状态码 -/// - 网络异常 => 返回失败 DTO -/// - JSON 反序列化为 null => 返回默认 DTO -/// - 取消 Token => 抛出 OperationCanceledException -/// - DownloadExtensionAsync(extensionId, savePath, progress, ct) -/// - 委托给 DownloadExtensionWithResultAsync -/// - 返回 result.Success -/// - DownloadExtensionWithResultAsync(extensionId, savePath, progress, ct) -/// - 成功下载 -/// - 断点续传_文件已存在时追加 -/// - HTTP 416 RangeNotSatisfiable => Ok() -/// - 客户端错误 4xx => Fail ClientError -/// - 服务器错误 5xx => Fail ServerError -/// - OperationCanceledException => Fail Cancelled -/// - HttpRequestException => Fail NetworkError -/// - IOException => Fail IoError -/// - 其他异常 => Fail Unknown -/// - progress 报告进度 -/// - Dispose() -/// - ownsHttpClient=true => 释放 HttpClient -/// - ownsHttpClient=false => 不释放 HttpClient -/// -using System.Net; -using System.Net.Http; -using Moq; -using Moq.Protected; -using Newtonsoft.Json; -using GeneralUpdate.Extension.Communication; -using GeneralUpdate.Extension.Common.DTOs; -using GeneralUpdate.Extension.Common.Models; - -namespace GeneralUpdate.Extension.Communication.Tests; - -public class ExtensionHttpClientTests -{ - private static Mock CreateHandlerMock(HttpResponseMessage response) - { - var handler = new Mock(); - handler.Protected() - .Setup>("SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .ReturnsAsync(response); - return handler; - } - - // ===== 构造函数测试 ===== - - [Fact] - public void 构造函数_ServerUrl为null_抛出ArgumentNullException() - { - Assert.Throws(() => - new ExtensionHttpClient(null!, "Bearer", "token")); - } - - [Fact] - public void 构造函数_HttpClient为null_抛出ArgumentNullException() - { - Assert.Throws(() => - new ExtensionHttpClient("http://test", "Bearer", "token", null!)); - } - - [Fact] - public void 构造函数_ServerUrl末尾斜杠被Trim() - { - using var httpClient = new HttpClient(); - var client = new ExtensionHttpClient("http://test.com/", "Bearer", "token", httpClient); - Assert.NotNull(client); - } - - [Fact] - public async Task 构造函数_Scheme和Token非空_设置AuthorizationHeader() - { - var handler = new Mock(); - HttpRequestMessage? capturedRequest = null; - handler.Protected() - .Setup>("SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Callback((req, _) => capturedRequest = req) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(JsonConvert.SerializeObject(new HttpResponseDTO>())) - }); - - using var httpClient = new HttpClient(handler.Object); - using var client = new ExtensionHttpClient("http://test", "Bearer", "my-token", httpClient); - - await client.QueryExtensionsAsync(new ExtensionQueryDTO()); - - Assert.NotNull(capturedRequest); - Assert.NotNull(capturedRequest!.Headers.Authorization); - Assert.Equal("Bearer", capturedRequest.Headers.Authorization.Scheme); - Assert.Equal("my-token", capturedRequest.Headers.Authorization.Parameter); - } - - [Fact] - public async Task 构造函数_Scheme为空_不设置Authorization() - { - var handler = new Mock(); - HttpRequestMessage? capturedRequest = null; - handler.Protected() - .Setup>("SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Callback((req, _) => capturedRequest = req) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(JsonConvert.SerializeObject(new HttpResponseDTO>())) - }); - - using var httpClient = new HttpClient(handler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - await client.QueryExtensionsAsync(new ExtensionQueryDTO()); - - Assert.Null(capturedRequest!.Headers.Authorization); - } - - // ===== QueryExtensionsAsync 测试 ===== - - [Fact] - public async Task QueryExtensionsAsync_成功响应_返回解析后的DTO() - { - var expectedDto = new HttpResponseDTO> - { - Code = "200", - Message = "OK", - Body = new PagedResultDTO - { - PageNumber = 1, - TotalCount = 1, - Items = new[] { new ExtensionDTO { Id = "ext-1", Name = "test" } } - } - }; - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(JsonConvert.SerializeObject(expectedDto)) - }; - var mockHandler = CreateHandlerMock(response); - using var httpClient = new HttpClient(mockHandler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - - var result = await client.QueryExtensionsAsync(new ExtensionQueryDTO { Id = "ext-1" }); - - Assert.NotNull(result); - Assert.Equal("OK", result.Message); - Assert.NotNull(result.Body); - Assert.Single(result.Body!.Items); - Assert.Equal("ext-1", result.Body.Items.First().Id); - } - - [Fact] - public async Task QueryExtensionsAsync_非成功状态码_返回错误DTO() - { - var response = new HttpResponseMessage(HttpStatusCode.NotFound) - { - Content = new StringContent("Not Found") - }; - var mockHandler = CreateHandlerMock(response); - using var httpClient = new HttpClient(mockHandler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - - var result = await client.QueryExtensionsAsync(new ExtensionQueryDTO()); - - Assert.NotNull(result); - Assert.Contains("404", result.Message); - } - - [Fact] - public async Task QueryExtensionsAsync_网络异常_返回错误DTO() - { - var handler = new Mock(); - handler.Protected() - .Setup>("SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .ThrowsAsync(new HttpRequestException("Network error")); - using var httpClient = new HttpClient(handler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - - var result = await client.QueryExtensionsAsync(new ExtensionQueryDTO()); - - Assert.NotNull(result); - Assert.Equal("QUERY_ERROR", result.Code); - Assert.Contains("Network error", result.Message); - } - - [Fact] - public async Task QueryExtensionsAsync_响应为nullJSON_返回默认DTO() - { - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("null") - }; - var mockHandler = CreateHandlerMock(response); - using var httpClient = new HttpClient(mockHandler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - - var result = await client.QueryExtensionsAsync(new ExtensionQueryDTO()); - Assert.NotNull(result); - } - - // ===== DownloadExtensionAsync 测试 ===== - - [Fact] - public async Task DownloadExtensionAsync_成功下载_返回true() - { - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new ByteArrayContent(new byte[] { 1, 2, 3 }) - }; - response.Content.Headers.ContentLength = 3; - var mockHandler = CreateHandlerMock(response); - using var httpClient = new HttpClient(mockHandler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - var savePath = Path.Combine(Path.GetTempPath(), $"dl-{Guid.NewGuid()}.zip"); - - var result = await client.DownloadExtensionAsync("ext-1", savePath); - - Assert.True(result); - Assert.True(File.Exists(savePath)); - // 清理 - try { File.Delete(savePath); } catch { } - } - - // ===== DownloadExtensionWithResultAsync 测试 ===== - - [Fact] - public async Task DownloadExtensionWithResultAsync_成功下载_返回Ok() - { - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new ByteArrayContent(new byte[] { 10, 20, 30 }) - }; - response.Content.Headers.ContentLength = 3; - var mockHandler = CreateHandlerMock(response); - using var httpClient = new HttpClient(mockHandler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - var savePath = Path.Combine(Path.GetTempPath(), $"dlr-{Guid.NewGuid()}.zip"); - - var result = await client.DownloadExtensionWithResultAsync("ext-1", savePath); - - Assert.True(result.Success); - Assert.Equal(DownloadErrorType.None, result.ErrorType); - try { File.Delete(savePath); } catch { } - } - - [Fact] - public async Task DownloadExtensionWithResultAsync_HTTP416_返回Ok() - { - var response = new HttpResponseMessage(HttpStatusCode.RequestedRangeNotSatisfiable); - var mockHandler = CreateHandlerMock(response); - using var httpClient = new HttpClient(mockHandler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - var savePath = Path.Combine(Path.GetTempPath(), $"dlr416-{Guid.NewGuid()}.zip"); - // 创建文件以模拟已有下载 - File.WriteAllText(savePath, "existing"); - - var result = await client.DownloadExtensionWithResultAsync("ext-1", savePath); - - Assert.True(result.Success); - try { File.Delete(savePath); } catch { } - } - - [Fact] - public async Task DownloadExtensionWithResultAsync_服务端500_返回Fail() - { - var response = new HttpResponseMessage(HttpStatusCode.InternalServerError) - { - Content = new StringContent("Server error"), - ReasonPhrase = "Internal Server Error" - }; - var mockHandler = CreateHandlerMock(response); - using var httpClient = new HttpClient(mockHandler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - var savePath = Path.Combine(Path.GetTempPath(), $"dlr500-{Guid.NewGuid()}.zip"); - - var result = await client.DownloadExtensionWithResultAsync("ext-1", savePath); - - Assert.False(result.Success); - Assert.Equal(DownloadErrorType.ServerError, result.ErrorType); - Assert.Equal(500, result.HttpStatusCode); - } - - [Fact] - public async Task DownloadExtensionWithResultAsync_客户端404_返回Fail() - { - var response = new HttpResponseMessage(HttpStatusCode.NotFound) - { - ReasonPhrase = "Not Found" - }; - var mockHandler = CreateHandlerMock(response); - using var httpClient = new HttpClient(mockHandler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - var savePath = Path.Combine(Path.GetTempPath(), $"dlr404-{Guid.NewGuid()}.zip"); - - var result = await client.DownloadExtensionWithResultAsync("ext-1", savePath); - - Assert.False(result.Success); - Assert.Equal(DownloadErrorType.ClientError, result.ErrorType); - } - - [Fact] - public async Task DownloadExtensionWithResultAsync_HttpRequestException_返回NetworkError() - { - var handler = new Mock(); - handler.Protected() - .Setup>("SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .ThrowsAsync(new HttpRequestException("Connection refused")); - using var httpClient = new HttpClient(handler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - var savePath = Path.Combine(Path.GetTempPath(), $"dlr-net-{Guid.NewGuid()}.zip"); - - var result = await client.DownloadExtensionWithResultAsync("ext-1", savePath); - - Assert.False(result.Success); - Assert.Equal(DownloadErrorType.NetworkError, result.ErrorType); - } - - [Fact] - public async Task DownloadExtensionWithResultAsync_OperationCanceledException_返回Cancelled() - { - var handler = new Mock(); - handler.Protected() - .Setup>("SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .ThrowsAsync(new OperationCanceledException()); - using var httpClient = new HttpClient(handler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - var savePath = Path.Combine(Path.GetTempPath(), $"dlr-cancel-{Guid.NewGuid()}.zip"); - - var result = await client.DownloadExtensionWithResultAsync("ext-1", savePath); - - Assert.False(result.Success); - Assert.Equal(DownloadErrorType.Cancelled, result.ErrorType); - } - - [Fact] - public async Task DownloadExtensionWithResultAsync_IOException_返回IoError() - { - var handler = new Mock(); - handler.Protected() - .Setup>("SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .ThrowsAsync(new IOException("Disk full")); - using var httpClient = new HttpClient(handler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - var savePath = Path.Combine(Path.GetTempPath(), $"dlr-io-{Guid.NewGuid()}.zip"); - - var result = await client.DownloadExtensionWithResultAsync("ext-1", savePath); - - Assert.False(result.Success); - Assert.Equal(DownloadErrorType.IoError, result.ErrorType); - } - - [Fact] - public async Task DownloadExtensionWithResultAsync_一般Exception_返回Unknown() - { - var handler = new Mock(); - handler.Protected() - .Setup>("SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .ThrowsAsync(new Exception("Unexpected error")); - using var httpClient = new HttpClient(handler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - var savePath = Path.Combine(Path.GetTempPath(), $"dlr-unk-{Guid.NewGuid()}.zip"); - - var result = await client.DownloadExtensionWithResultAsync("ext-1", savePath); - - Assert.False(result.Success); - Assert.Equal(DownloadErrorType.Unknown, result.ErrorType); - } - - [Fact] - public async Task DownloadExtensionWithResultAsync_进度报告正确() - { - var bytes = new byte[8192 * 3]; // 3 buffers worth - new Random(42).NextBytes(bytes); - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new ByteArrayContent(bytes) - }; - response.Content.Headers.ContentLength = bytes.Length; - var mockHandler = CreateHandlerMock(response); - using var httpClient = new HttpClient(mockHandler.Object); - using var client = new ExtensionHttpClient("http://test", "", "", httpClient); - var savePath = Path.Combine(Path.GetTempPath(), $"dlr-prog-{Guid.NewGuid()}.zip"); - - var progressValues = new List(); - var progress = new Progress(p => progressValues.Add(p)); - - var result = await client.DownloadExtensionWithResultAsync("ext-1", savePath, progress); - - Assert.True(result.Success); - Assert.NotEmpty(progressValues); - Assert.Contains(100, progressValues); - try { File.Delete(savePath); } catch { } - } - - // ===== Dispose 测试 ===== - - [Fact] - public void Dispose_ownsHttpClient为true_释放HttpClient() - { - var handler = new Mock(); - var httpClient = new HttpClient(handler.Object); - var client = new ExtensionHttpClient("http://test", "", "", httpClient, ownsHttpClient: true); - client.Dispose(); - // HttpClient 被释放后,再发送请求会抛异常 - Assert.Throws(() => httpClient.Timeout = TimeSpan.FromSeconds(1)); - } - - [Fact] - public void Dispose_ownsHttpClient为false_不释放HttpClient() - { - var handler = new Mock(); - var httpClient = new HttpClient(handler.Object); - var client = new ExtensionHttpClient("http://test", "", "", httpClient, ownsHttpClient: false); - client.Dispose(); - // HttpClient 仍可用 - httpClient.Timeout = TimeSpan.FromSeconds(5); - } - - [Fact] - public void 便利构造函数_ownsHttpClient为true() - { - var client = new ExtensionHttpClient("http://test", "Bearer", "token"); - client.Dispose(); // 应释放内部创建的 HttpClient - } -} diff --git a/src/c#/ExtensionTest/Core/GeneralExtensionHostTests.cs b/src/c#/ExtensionTest/Core/GeneralExtensionHostTests.cs deleted file mode 100644 index 4f8203b6..00000000 --- a/src/c#/ExtensionTest/Core/GeneralExtensionHostTests.cs +++ /dev/null @@ -1,589 +0,0 @@ -/// -/// 测试覆盖点: -/// - DI构造函数 -/// - options=null => ArgumentNullException -/// - httpClient=null => ArgumentNullException -/// - catalog=null => ArgumentNullException -/// - compatibilityChecker=null => ArgumentNullException -/// - downloadQueue=null => ArgumentNullException -/// - platformMatcher=null => ArgumentNullException -/// - dependencyResolver=null => 不抛异常_注释掉的检查 -/// - lifecycleHooks/metadataMapper 可选参数可为 null -/// - 订阅 DownloadStatusChanged 事件 -/// - 创建目录 -/// - Legacy构造函数 -/// - options=null => ArgumentNullException -/// - ExtensionCatalog 属性 -/// - QueryExtensionsAsync(query) -/// - 正常查询返回结果 -/// - 查询抛异常_异常继续向上传播 -/// - DownloadExtensionAsync(extensionId, savePath) -/// - 成功下载返回 true -/// - 失败下载返回 false -/// - UpdateExtensionAsync(extensionId) -/// - 成功更新完整流程_查询 + 兼容性检查 + 平台检查 + 依赖解析 + 下载 + 安装 + 编目更新 -/// - 服务器返回 null items => 抛异常 -/// - 服务器无此扩展 => InvalidOperationException -/// - 不兼容 => InvalidOperationException -/// - 平台不支持 => InvalidOperationException -/// - 有未安装依赖 => 递归安装 -/// - 下载失败 => 抛异常 -/// - 安装失败 => 抛异常 -/// - 事件触发: Queued -> Updating(progress) -> UpdateSuccessful(progress=100) -/// - 异常时: Queued -> UpdateFailed -/// - 返回 false 但事件通知失败 -/// - InstallExtensionAsync(extensionPath, rollbackOnFailure) -/// - 文件不存在 => FileNotFoundException -/// - 非 .zip 文件 => InvalidOperationException -/// - lifecycleHooks.OnBeforeInstallAsync 返回 false => 取消安装 -/// - 正常安装流程 -/// - 已存在同名扩展且有rollback => 备份->删除旧->解压新->删除备份 -/// - rollbackOnFailure=false => 跳过备份 -/// - 安装失败且rollback => 恢复备份 -/// - UpdateExtensionsAsync(extensionIds, ct) -/// - CancellationToken 取消 -/// - 单个失败不影响其他 -/// - IsExtensionCompatible(extension) -/// - SetAutoUpdate / IsAutoUpdateEnabled -/// - 单个扩展设置 -/// - 全局设置兜底 -/// - SetGlobalAutoUpdate -/// - ExtensionUpdateStatusChanged 事件 -/// - SafeExtractZipAsync_内部方法难以直接测试 -/// - ComputeFileSha256Async_内部方法难以直接测试 -/// - SafeDeleteFile_内部方法难以直接测试 -/// - ToMetadata 静态方法_DTO -> Metadata 映射 -/// -using Moq; -using GeneralUpdate.Extension.Core; -using GeneralUpdate.Extension.Catalog; -using GeneralUpdate.Extension.Communication; -using GeneralUpdate.Extension.Compatibility; -using GeneralUpdate.Extension.Dependencies; -using GeneralUpdate.Extension.Download; -using GeneralUpdate.Extension.Common.DTOs; -using GeneralUpdate.Extension.Common.Enums; -using GeneralUpdate.Extension.Common.Models; - -namespace GeneralUpdate.Extension.Core.Tests; - -public class GeneralExtensionHostTests -{ - private static ExtensionHostOptions CreateOptions(string? extDir = null) - { - return new ExtensionHostOptions - { - ServerUrl = "http://test-server", - Scheme = "Bearer", - Token = "test-token", - HostVersion = "2.0.0", - ExtensionsDirectory = extDir ?? Path.Combine(Path.GetTempPath(), $"host-test-{Guid.NewGuid()}") - }; - } - - private static Mock CreateHttpClientMock() - { - return new Mock(); - } - - private static Mock CreateCatalogMock() - { - var mock = new Mock(); - mock.Setup(m => m.GetInstalledExtensions()).Returns(new List()); - mock.Setup(m => m.GetInstalledExtensionById(It.IsAny())).Returns((ExtensionMetadata?)null); - return mock; - } - - // ===== DI构造函数测试 ===== - - [Fact] - public void DI构造函数_options为null_抛出ArgumentNullException() - { - Assert.Throws(() => - new GeneralExtensionHost(null!, CreateHttpClientMock().Object, CreateCatalogMock().Object, - Mock.Of(), Mock.Of(), - Mock.Of(), Mock.Of())); - } - - [Fact] - public void DI构造函数_httpClient为null_抛出ArgumentNullException() - { - var opts = CreateOptions(); - Assert.Throws(() => - new GeneralExtensionHost(opts, null!, CreateCatalogMock().Object, - Mock.Of(), Mock.Of(), - Mock.Of(), Mock.Of())); - } - - [Fact] - public void DI构造函数_catalog为null_抛出ArgumentNullException() - { - var opts = CreateOptions(); - Assert.Throws(() => - new GeneralExtensionHost(opts, CreateHttpClientMock().Object, null!, - Mock.Of(), Mock.Of(), - Mock.Of(), Mock.Of())); - } - - [Fact] - public void DI构造函数_compatibilityChecker为null_抛出ArgumentNullException() - { - var opts = CreateOptions(); - Assert.Throws(() => - new GeneralExtensionHost(opts, CreateHttpClientMock().Object, CreateCatalogMock().Object, - null!, Mock.Of(), - Mock.Of(), Mock.Of())); - } - - [Fact] - public void DI构造函数_downloadQueue为null_抛出ArgumentNullException() - { - var opts = CreateOptions(); - Assert.Throws(() => - new GeneralExtensionHost(opts, CreateHttpClientMock().Object, CreateCatalogMock().Object, - Mock.Of(), null!, - Mock.Of(), Mock.Of())); - } - - [Fact] - public void DI构造函数_platformMatcher为null_抛出ArgumentNullException() - { - var opts = CreateOptions(); - Assert.Throws(() => - new GeneralExtensionHost(opts, CreateHttpClientMock().Object, CreateCatalogMock().Object, - Mock.Of(), Mock.Of(), - Mock.Of(), null!)); - } - - [Fact] - public void DI构造函数_dependencyResolver为null_不抛异常() - { - var opts = CreateOptions(); - // dependencyResolver 的 null 检查被注释掉了,所以不应抛异常 - var host = new GeneralExtensionHost(opts, CreateHttpClientMock().Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), null!, Mock.Of()); - Assert.NotNull(host); - } - - [Fact] - public void DI构造函数_可选参数为null_可正常构建() - { - var opts = CreateOptions(); - var host = new GeneralExtensionHost(opts, CreateHttpClientMock().Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of(), lifecycleHooks: null, metadataMapper: null); - Assert.NotNull(host); - } - - [Fact] - public void DI构造函数_正常构建_ExtensionCatalog属性可用() - { - var opts = CreateOptions(); - var catalogMock = CreateCatalogMock(); - var host = new GeneralExtensionHost(opts, CreateHttpClientMock().Object, - catalogMock.Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - Assert.NotNull(host.ExtensionCatalog); - Assert.Same(catalogMock.Object, host.ExtensionCatalog); - } - - // ===== Legacy构造函数测试 ===== - - [Fact] - public void Legacy构造函数_options为null_抛出ArgumentNullException() - { - Assert.Throws(() => new GeneralExtensionHost(null!)); - } - - [Fact] - public void Legacy构造函数_正常构建() - { - var opts = CreateOptions(); - var host = new GeneralExtensionHost(opts); - Assert.NotNull(host); - Assert.NotNull(host.ExtensionCatalog); - } - - // ===== QueryExtensionsAsync 测试 ===== - - [Fact] - public async Task QueryExtensionsAsync_正常查询_返回结果() - { - var expectedResponse = new HttpResponseDTO> - { - Code = "200", - Body = new PagedResultDTO - { - Items = new[] { new ExtensionDTO { Id = "ext-1" } } - } - }; - var httpMock = CreateHttpClientMock(); - httpMock.Setup(m => m.QueryExtensionsAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(expectedResponse); - - var host = new GeneralExtensionHost(CreateOptions(), httpMock.Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - var result = await host.QueryExtensionsAsync(new ExtensionQueryDTO { Id = "ext-1" }); - Assert.NotNull(result); - Assert.Equal("200", result.Code); - } - - [Fact] - public async Task QueryExtensionsAsync_查询抛异常_异常向上传播() - { - var httpMock = CreateHttpClientMock(); - httpMock.Setup(m => m.QueryExtensionsAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new HttpRequestException("Network error")); - - var host = new GeneralExtensionHost(CreateOptions(), httpMock.Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - await Assert.ThrowsAsync(() => - host.QueryExtensionsAsync(new ExtensionQueryDTO())); - } - - // ===== DownloadExtensionAsync 测试 ===== - - [Fact] - public async Task DownloadExtensionAsync_成功返回true() - { - var httpMock = CreateHttpClientMock(); - httpMock.Setup(m => m.DownloadExtensionAsync( - "ext-1", It.IsAny(), It.IsAny>(), It.IsAny())) - .ReturnsAsync(true); - - var host = new GeneralExtensionHost(CreateOptions(), httpMock.Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - var result = await host.DownloadExtensionAsync("ext-1", "/tmp/test.zip"); - Assert.True(result); - } - - [Fact] - public async Task DownloadExtensionAsync_失败返回false() - { - var httpMock = CreateHttpClientMock(); - httpMock.Setup(m => m.DownloadExtensionAsync( - "ext-1", It.IsAny(), It.IsAny>(), It.IsAny())) - .ReturnsAsync(false); - - var host = new GeneralExtensionHost(CreateOptions(), httpMock.Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - var result = await host.DownloadExtensionAsync("ext-1", "/tmp/test.zip"); - Assert.False(result); - } - - // ===== IsExtensionCompatible 测试 ===== - - [Fact] - public void IsExtensionCompatible_委托给CompatibilityChecker() - { - var compatMock = new Mock(); - compatMock.Setup(m => m.IsCompatible(It.IsAny(), "2.0.0")) - .Returns(true); - - var host = new GeneralExtensionHost(CreateOptions(), CreateHttpClientMock().Object, - CreateCatalogMock().Object, compatMock.Object, Mock.Of(), - Mock.Of(), Mock.Of()); - - Assert.True(host.IsExtensionCompatible(new ExtensionMetadata())); - } - - // ===== SetAutoUpdate / IsAutoUpdateEnabled / SetGlobalAutoUpdate ===== - - [Fact] - public void SetAutoUpdate_单个扩展设置() - { - var host = new GeneralExtensionHost(CreateOptions(), CreateHttpClientMock().Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - host.SetAutoUpdate("ext-1", true); - Assert.True(host.IsAutoUpdateEnabled("ext-1")); - - host.SetAutoUpdate("ext-1", false); - Assert.False(host.IsAutoUpdateEnabled("ext-1")); - } - - [Fact] - public void IsAutoUpdateEnabled_无单独设置_使用全局设置() - { - var host = new GeneralExtensionHost(CreateOptions(), CreateHttpClientMock().Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - // 默认全局为 false - Assert.False(host.IsAutoUpdateEnabled("any-ext")); - - host.SetGlobalAutoUpdate(true); - Assert.True(host.IsAutoUpdateEnabled("any-ext")); - } - - [Fact] - public void IsAutoUpdateEnabled_单独设置覆盖全局设置() - { - var host = new GeneralExtensionHost(CreateOptions(), CreateHttpClientMock().Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - host.SetGlobalAutoUpdate(true); - host.SetAutoUpdate("ext-special", false); - - Assert.False(host.IsAutoUpdateEnabled("ext-special")); // 单独设置优先 - Assert.True(host.IsAutoUpdateEnabled("other-ext")); // 全局设置兜底 - } - - // ===== ExtensionUpdateStatusChanged 事件测试 ===== - - [Fact] - public async Task Download_触发进度事件() - { - var httpMock = CreateHttpClientMock(); - httpMock.Setup(m => m.DownloadExtensionAsync( - It.IsAny(), It.IsAny(), - It.IsAny>(), It.IsAny())) - .Returns, CancellationToken>((id, path, progress, ct) => - { - progress?.Report(50); - return Task.FromResult(true); - }); - - var host = new GeneralExtensionHost(CreateOptions(), httpMock.Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - var events = new List(); - host.ExtensionUpdateStatusChanged += (_, e) => events.Add(e); - - await host.DownloadExtensionAsync("ext-1", "/tmp/test.zip"); - - Assert.NotEmpty(events); - Assert.Contains(events, e => e.Status == ExtensionUpdateStatus.Updating && e.Progress == 50); - } - - // ===== UpdateExtensionAsync 测试 ===== - - [Fact] - public async Task UpdateExtensionAsync_服务器返回nullItems_抛出异常_事件通知失败() - { - var httpMock = CreateHttpClientMock(); - httpMock.Setup(m => m.QueryExtensionsAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new HttpResponseDTO> { Body = null }); - - var host = new GeneralExtensionHost(CreateOptions(), httpMock.Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - var failedEventReceived = false; - host.ExtensionUpdateStatusChanged += (_, e) => - { - if (e.Status == ExtensionUpdateStatus.UpdateFailed) failedEventReceived = true; - }; - - var result = await host.UpdateExtensionAsync("ext-1"); - - Assert.False(result); - Assert.True(failedEventReceived); - } - - [Fact] - public async Task UpdateExtensionAsync_服务器无此扩展_返回false() - { - var httpMock = CreateHttpClientMock(); - httpMock.Setup(m => m.QueryExtensionsAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new HttpResponseDTO> - { - Body = new PagedResultDTO - { - Items = new[] { new ExtensionDTO { Id = "other-ext" } } - } - }); - - var host = new GeneralExtensionHost(CreateOptions(), httpMock.Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - var result = await host.UpdateExtensionAsync("ext-1"); - Assert.False(result); - } - - [Fact] - public async Task UpdateExtensionAsync_不兼容_返回false() - { - var httpMock = CreateHttpClientMock(); - httpMock.Setup(m => m.QueryExtensionsAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new HttpResponseDTO> - { - Body = new PagedResultDTO - { - Items = new[] { new ExtensionDTO { Id = "ext-1", Name = "ext", MinHostVersion = "5.0.0" } } - } - }); - - var compatMock = new Mock(); - compatMock.Setup(m => m.IsCompatible(It.IsAny(), "2.0.0")) - .Returns(false); - - var host = new GeneralExtensionHost(CreateOptions(), httpMock.Object, - CreateCatalogMock().Object, compatMock.Object, Mock.Of(), - Mock.Of(), Mock.Of()); - - var result = await host.UpdateExtensionAsync("ext-1"); - Assert.False(result); - } - - [Fact] - public async Task UpdateExtensionAsync_平台不支持_返回false() - { - var httpMock = CreateHttpClientMock(); - httpMock.Setup(m => m.QueryExtensionsAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new HttpResponseDTO> - { - Body = new PagedResultDTO - { - Items = new[] { new ExtensionDTO { Id = "ext-1", Name = "ext", SupportedPlatforms = TargetPlatform.Linux } } - } - }); - - var compatMock = new Mock(); - compatMock.Setup(m => m.IsCompatible(It.IsAny(), "2.0.0")).Returns(true); - var platformMock = new Mock(); - platformMock.Setup(m => m.IsCurrentPlatformSupported(It.IsAny())).Returns(false); - - var host = new GeneralExtensionHost(CreateOptions(), httpMock.Object, - CreateCatalogMock().Object, compatMock.Object, Mock.Of(), - Mock.Of(), platformMock.Object); - - var result = await host.UpdateExtensionAsync("ext-1"); - Assert.False(result); - } - - // ===== UpdateExtensionsAsync 测试 ===== - - [Fact] - public async Task UpdateExtensionsAsync_CancellationToken取消() - { - var cts = new CancellationTokenSource(); - cts.Cancel(); - - var host = new GeneralExtensionHost(CreateOptions(), CreateHttpClientMock().Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - await Assert.ThrowsAsync(() => - host.UpdateExtensionsAsync(new[] { "ext-1" }, cts.Token)); - } - - [Fact] - public async Task UpdateExtensionsAsync_单个失败不影响其他() - { - var httpMock = CreateHttpClientMock(); - // 第一个查询返回空_导致失败,第二个成功 - httpMock.SetupSequence(m => m.QueryExtensionsAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new HttpResponseDTO> { Body = null }) - .ReturnsAsync(new HttpResponseDTO> - { - Body = new PagedResultDTO - { - Items = new[] { new ExtensionDTO { Id = "ext-2", Name = "ext2" } } - } - }); - - var compatMock = new Mock(); - compatMock.Setup(m => m.IsCompatible(It.IsAny(), "2.0.0")).Returns(false); - - var host = new GeneralExtensionHost(CreateOptions(), httpMock.Object, - CreateCatalogMock().Object, compatMock.Object, Mock.Of(), - Mock.Of(), Mock.Of()); - - var result = await host.UpdateExtensionsAsync(new[] { "ext-1", "ext-2" }); - - Assert.False(result["ext-1"]); // 第一个失败_不兼容 - Assert.False(result["ext-2"]); // 第二个也失败_不兼容 - } - - // ===== InstallExtensionAsync 测试 ===== - - [Fact] - public async Task InstallExtensionAsync_文件不存在_抛FileNotFoundException_返回false() - { - var host = new GeneralExtensionHost(CreateOptions(), CreateHttpClientMock().Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - var result = await host.InstallExtensionAsync(@"C:\nonexistent\file.zip"); - Assert.False(result); - } - - [Fact] - public async Task InstallExtensionAsync_非zip文件_返回false() - { - var tempFile = Path.GetTempFileName(); - await File.WriteAllTextAsync(tempFile, "not a zip"); - - var host = new GeneralExtensionHost(CreateOptions(), CreateHttpClientMock().Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); - - var result = await host.InstallExtensionAsync(tempFile); - Assert.False(result); - - try { File.Delete(tempFile); } catch { } - } - - [Fact] - public async Task InstallExtensionAsync_lifecycleHook返回false_取消安装_返回false() - { - // 创建一个有效的最小 .zip 文件 - var tempZip = Path.Combine(Path.GetTempPath(), $"test-ext_{Guid.NewGuid()}.zip"); - CreateMinimalZipFile(tempZip); - - var lifecycleMock = new Mock(); - lifecycleMock.Setup(m => m.OnBeforeInstallAsync( - It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(false); - - var host = new GeneralExtensionHost(CreateOptions(), CreateHttpClientMock().Object, - CreateCatalogMock().Object, Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of(), lifecycleMock.Object); - - var result = await host.InstallExtensionAsync(tempZip); - Assert.False(result); - - try { File.Delete(tempZip); } catch { } - } - - // ===== 辅助方法 ===== - - private static void CreateMinimalZipFile(string path) - { - // 创建一个包含单个空文本文件的最小 zip 文件 - using var archive = System.IO.Compression.ZipFile.Open(path, System.IO.Compression.ZipArchiveMode.Create); - var entry = archive.CreateEntry("readme.txt"); - using var writer = new StreamWriter(entry.Open()); - writer.Write("test"); - } -} diff --git a/src/c#/ExtensionTest/Dependencies/DependencyResolverTests.cs b/src/c#/ExtensionTest/Dependencies/DependencyResolverTests.cs deleted file mode 100644 index c9808939..00000000 --- a/src/c#/ExtensionTest/Dependencies/DependencyResolverTests.cs +++ /dev/null @@ -1,249 +0,0 @@ -/// -/// 测试覆盖点: -/// - ResolveDependencies(extension) -/// - extension.Id 不在已安装列表中 => 返回仅含 [extension.Id] -/// - 无依赖 => 返回 [extension.Id] -/// - 单层依赖 => 返回 [dep, extension.Id]_拓扑序:先依赖后自身 -/// - 多层依赖链 => 正确的拓扑序 -/// - 循环依赖检测 => 抛出 InvalidOperationException -/// - 多子依赖共享依赖 => 依赖只出现一次 -/// - GetMissingDependencies(extension) -/// - 全部依赖已安装 => 返回空列表 -/// - 部分依赖缺失 => 返回缺失ID列表 -/// - 全部依赖缺失 => 返回全部依赖ID列表 -/// - 无依赖 => 返回空列表 -/// -using Moq; -using GeneralUpdate.Extension.Dependencies; -using GeneralUpdate.Extension.Catalog; -using GeneralUpdate.Extension.Common.Models; - -namespace GeneralUpdate.Extension.Dependencies.Tests; - -public class DependencyResolverTests -{ - private Mock CreateCatalogMock(Dictionary installed) - { - var mock = new Mock(); - mock.Setup(m => m.GetInstalledExtensionById(It.IsAny())) - .Returns(id => installed.TryGetValue(id, out var ext) ? ext : null); - return mock; - } - - // ===== ResolveDependencies 测试 ===== - - [Fact] - public void ResolveDependencies_扩展不在已安装目录中_返回仅含自身() - { - var catalogMock = CreateCatalogMock(new Dictionary()); - // extension.Id="ext-a",但目录中没有,所以递归直接跳过 - var ext = new ExtensionMetadata { Id = "ext-a" }; - var resolver = new DependencyResolver(catalogMock.Object); - - var result = resolver.ResolveDependencies(ext); - Assert.Single(result); - Assert.Equal("ext-a", result[0]); - } - - [Fact] - public void ResolveDependencies_无依赖扩展_返回仅含自身() - { - var meta = new ExtensionMetadata { Id = "ext-a" }; - var catalog = new Dictionary - { - ["ext-a"] = meta - }; - var catalogMock = CreateCatalogMock(catalog); - var resolver = new DependencyResolver(catalogMock.Object); - - var result = resolver.ResolveDependencies(meta); - Assert.Single(result); - Assert.Equal("ext-a", result[0]); - } - - [Fact] - public void ResolveDependencies_单层依赖_依赖先于自身() - { - var dep = new ExtensionMetadata { Id = "dep-b" }; - var ext = new ExtensionMetadata { Id = "ext-a", Dependencies = "dep-b" }; - var catalog = new Dictionary - { - ["ext-a"] = ext, - ["dep-b"] = dep - }; - var catalogMock = CreateCatalogMock(catalog); - var resolver = new DependencyResolver(catalogMock.Object); - - var result = resolver.ResolveDependencies(ext); - Assert.Equal(2, result.Count); - Assert.Equal("dep-b", result[0]); // 依赖先于扩展 - Assert.Equal("ext-a", result[1]); - } - - [Fact] - public void ResolveDependencies_多层依赖链_正确拓扑序() - { - var depC = new ExtensionMetadata { Id = "dep-c" }; - var depB = new ExtensionMetadata { Id = "dep-b", Dependencies = "dep-c" }; - var extA = new ExtensionMetadata { Id = "ext-a", Dependencies = "dep-b" }; - var catalog = new Dictionary - { - ["ext-a"] = extA, - ["dep-b"] = depB, - ["dep-c"] = depC - }; - var catalogMock = CreateCatalogMock(catalog); - var resolver = new DependencyResolver(catalogMock.Object); - - var result = resolver.ResolveDependencies(extA); - Assert.Equal(3, result.Count); - Assert.Equal("dep-c", result[0]); - Assert.Equal("dep-b", result[1]); - Assert.Equal("ext-a", result[2]); - } - - [Fact] - public void ResolveDependencies_多个直接依赖_全部解析() - { - var dep1 = new ExtensionMetadata { Id = "dep-1" }; - var dep2 = new ExtensionMetadata { Id = "dep-2" }; - var ext = new ExtensionMetadata { Id = "ext-x", Dependencies = "dep-1,dep-2" }; - var catalog = new Dictionary - { - ["ext-x"] = ext, - ["dep-1"] = dep1, - ["dep-2"] = dep2 - }; - var catalogMock = CreateCatalogMock(catalog); - var resolver = new DependencyResolver(catalogMock.Object); - - var result = resolver.ResolveDependencies(ext); - Assert.Equal(3, result.Count); - Assert.Contains("dep-1", result); - Assert.Contains("dep-2", result); - Assert.Equal("ext-x", result[^1]); // 自身在最后 - } - - [Fact] - public void ResolveDependencies_共享依赖_只出现一次() - { - var sharedDep = new ExtensionMetadata { Id = "shared" }; - var depA = new ExtensionMetadata { Id = "dep-a", Dependencies = "shared" }; - var depB = new ExtensionMetadata { Id = "dep-b", Dependencies = "shared" }; - var ext = new ExtensionMetadata { Id = "root", Dependencies = "dep-a,dep-b" }; - var catalog = new Dictionary - { - ["root"] = ext, - ["dep-a"] = depA, - ["dep-b"] = depB, - ["shared"] = sharedDep - }; - var catalogMock = CreateCatalogMock(catalog); - var resolver = new DependencyResolver(catalogMock.Object); - - var result = resolver.ResolveDependencies(ext); - Assert.Equal(4, result.Count); - Assert.Single(result.Where(id => id == "shared")); - } - - [Fact] - public void ResolveDependencies_循环依赖_抛出InvalidOperationException() - { - var depB = new ExtensionMetadata { Id = "dep-b", Dependencies = "dep-a" }; - var depA = new ExtensionMetadata { Id = "dep-a", Dependencies = "dep-b" }; - var ext = new ExtensionMetadata { Id = "root", Dependencies = "dep-a" }; - var catalog = new Dictionary - { - ["root"] = ext, - ["dep-a"] = depA, - ["dep-b"] = depB - }; - var catalogMock = CreateCatalogMock(catalog); - var resolver = new DependencyResolver(catalogMock.Object); - - var ex = Assert.Throws(() => resolver.ResolveDependencies(ext)); - Assert.Contains("Circular dependency", ex.Message); - } - - [Fact] - public void ResolveDependencies_自循环依赖_抛出异常() - { - var ext = new ExtensionMetadata { Id = "self-loop", Dependencies = "self-loop" }; - var catalog = new Dictionary { ["self-loop"] = ext }; - var catalogMock = CreateCatalogMock(catalog); - var resolver = new DependencyResolver(catalogMock.Object); - - Assert.Throws(() => resolver.ResolveDependencies(ext)); - } - - // ===== GetMissingDependencies 测试 ===== - - [Fact] - public void GetMissingDependencies_全部已安装_返回空列表() - { - var dep = new ExtensionMetadata { Id = "dep-1" }; - var ext = new ExtensionMetadata { Id = "ext-a", Dependencies = "dep-1" }; - var catalog = new Dictionary - { - ["ext-a"] = ext, - ["dep-1"] = dep - }; - var catalogMock = CreateCatalogMock(catalog); - var resolver = new DependencyResolver(catalogMock.Object); - - var missing = resolver.GetMissingDependencies(ext); - Assert.Empty(missing); - } - - [Fact] - public void GetMissingDependencies_部分缺失_返回缺失ID() - { - var ext = new ExtensionMetadata { Id = "ext-a", Dependencies = "missing-dep" }; - var catalog = new Dictionary { ["ext-a"] = ext }; - var catalogMock = CreateCatalogMock(catalog); - var resolver = new DependencyResolver(catalogMock.Object); - - var missing = resolver.GetMissingDependencies(ext); - Assert.Single(missing); - Assert.Equal("missing-dep", missing[0]); - } - - [Fact] - public void GetMissingDependencies_全部依赖缺失_返回全部依赖() - { - var depB = new ExtensionMetadata { Id = "dep-b" }; // dep-b 已安装,但 dep-b 依赖 dep-c 缺失 - // 不将 dep-b 加入 catalog,让它查询返回 null_但GetMissingDependencies调用ResolveDependencies时需要catalog数据 - var ext = new ExtensionMetadata { Id = "ext-a", Dependencies = "dep-b" }; - // dep-b 不在catalog中 - var catalog = new Dictionary { ["ext-a"] = ext }; - var catalogMock = CreateCatalogMock(catalog); - var resolver = new DependencyResolver(catalogMock.Object); - - var missing = resolver.GetMissingDependencies(ext); - Assert.Single(missing); - Assert.Equal("dep-b", missing[0]); - } - - [Fact] - public void GetMissingDependencies_无依赖_返回空列表() - { - var ext = new ExtensionMetadata { Id = "ext-a" }; - var catalogMock = CreateCatalogMock(new Dictionary()); - var resolver = new DependencyResolver(catalogMock.Object); - - Assert.Empty(resolver.GetMissingDependencies(ext)); - } - - [Fact] - public void ResolveDependencies_依赖在catalog中找不到_跳过该依赖() - { - var ext = new ExtensionMetadata { Id = "root", Dependencies = "unknown-dep" }; - var catalog = new Dictionary { ["root"] = ext }; - var catalogMock = CreateCatalogMock(catalog); - var resolver = new DependencyResolver(catalogMock.Object); - - var result = resolver.ResolveDependencies(ext); - Assert.Single(result); - Assert.Equal("root", result[0]); - } -} diff --git a/src/c#/ExtensionTest/Download/DownloadQueueManagerTests.cs b/src/c#/ExtensionTest/Download/DownloadQueueManagerTests.cs deleted file mode 100644 index 09603a09..00000000 --- a/src/c#/ExtensionTest/Download/DownloadQueueManagerTests.cs +++ /dev/null @@ -1,287 +0,0 @@ -/// -/// 测试覆盖点: -/// - 构造函数 -/// - 默认 maxConcurrentDownloads=3 -/// - 指定 maxConcurrentDownloads -/// - Enqueue(task) -/// - Status 设为 Queued -/// - 添加到 activeTasks -/// - 触发 DownloadStatusChanged 事件 -/// - 启动队列处理 -/// - GetTask(extensionId) -/// - 已存在任务 => 返回该任务 -/// - 不存在任务 => 返回 null -/// - CancelTask(extensionId) -/// - 存在任务 => 调用 CancellationTokenSource.Cancel() -/// - 不存在任务 => 不抛异常 -/// - GetActiveTasks() -/// - 空队列 => 返回空列表 -/// - 有任务 => 返回所有活动任务 -/// - Dispose() -/// - 取消所有任务 -/// - 清空队列 -/// - 释放 semaphore -/// - 多次 Dispose 不会重复执行 -/// - ProcessQueueAsync -/// - 正常处理流程:Queued -> Updating -> UpdateSuccessful -/// - 任务取消:OperationCanceledException -> UpdateFailed -/// - 任务异常:Exception -> UpdateFailed -/// -using Moq; -using GeneralUpdate.Extension.Download; -using GeneralUpdate.Extension.Common.Enums; -using GeneralUpdate.Extension.Common.Models; - -namespace GeneralUpdate.Extension.Download.Tests; - -public class DownloadQueueManagerTests -{ - private DownloadTask CreateTask(string id = "ext-1", string name = "test-ext") - { - return new DownloadTask - { - Extension = new ExtensionMetadata { Id = id, Name = name }, - SavePath = $@"C:\temp\{id}.zip" - }; - } - - // ===== 构造函数 ===== - - [Fact] - public void 构造函数_默认maxConcurrentDownloads为3() - { - using var qm = new DownloadQueueManager(); - Assert.NotNull(qm); - } - - [Fact] - public void 构造函数_指定maxConcurrentDownloads() - { - using var qm = new DownloadQueueManager(5); - Assert.NotNull(qm); - } - - // ===== Enqueue ===== - - [Fact] - public void Enqueue_任务Status设为Queued() - { - using var qm = new DownloadQueueManager(); - var task = CreateTask(); - qm.Enqueue(task); - Assert.Equal(ExtensionUpdateStatus.Queued, task.Status); - } - - [Fact] - public void Enqueue_触发DownloadStatusChanged事件() - { - using var qm = new DownloadQueueManager(); - var task = CreateTask(); - DownloadTaskEventArgs? eventArgs = null; - qm.DownloadStatusChanged += (_, e) => eventArgs = e; - - qm.Enqueue(task); - - Assert.NotNull(eventArgs); - Assert.Same(task, eventArgs!.Task); - Assert.Equal(ExtensionUpdateStatus.Queued, eventArgs.Task.Status); - } - - [Fact] - public void Enqueue_多个任务依次入队() - { - using var qm = new DownloadQueueManager(); - var task1 = CreateTask("ext-1"); - var task2 = CreateTask("ext-2"); - qm.Enqueue(task1); - qm.Enqueue(task2); - - var active = qm.GetActiveTasks(); - Assert.Equal(2, active.Count); - } - - // ===== GetTask ===== - - [Fact] - public void GetTask_存在任务_返回该任务() - { - using var qm = new DownloadQueueManager(); - var task = CreateTask(); - qm.Enqueue(task); - - var found = qm.GetTask("ext-1"); - Assert.NotNull(found); - Assert.Same(task, found); - } - - [Fact] - public void GetTask_不存在任务_返回null() - { - using var qm = new DownloadQueueManager(); - Assert.Null(qm.GetTask("nonexistent")); - } - - // ===== CancelTask ===== - - [Fact] - public void CancelTask_存在任务_取消Token() - { - using var qm = new DownloadQueueManager(); - var task = CreateTask(); - qm.Enqueue(task); - Assert.False(task.CancellationTokenSource.IsCancellationRequested); - - qm.CancelTask("ext-1"); - Assert.True(task.CancellationTokenSource.IsCancellationRequested); - } - - [Fact] - public void CancelTask_不存在任务_不抛异常() - { - using var qm = new DownloadQueueManager(); - // 不应该抛出异常 - qm.CancelTask("nonexistent"); - } - - // ===== GetActiveTasks ===== - - [Fact] - public void GetActiveTasks_空队列_返回空列表() - { - using var qm = new DownloadQueueManager(); - Assert.Empty(qm.GetActiveTasks()); - } - - [Fact] - public void GetActiveTasks_返回所有活动任务() - { - using var qm = new DownloadQueueManager(); - var t1 = CreateTask("e1"); - var t2 = CreateTask("e2"); - qm.Enqueue(t1); - qm.Enqueue(t2); - - var active = qm.GetActiveTasks(); - Assert.Contains(t1, active); - Assert.Contains(t2, active); - } - - // ===== Dispose ===== - - [Fact] - public void Dispose_取消所有活动任务() - { - var qm = new DownloadQueueManager(); - var task = CreateTask(); - qm.Enqueue(task); - - qm.Dispose(); - - Assert.True(task.CancellationTokenSource.IsCancellationRequested); - } - - [Fact] - public void Dispose_清空活动任务列表() - { - var qm = new DownloadQueueManager(); - qm.Enqueue(CreateTask("e1")); - qm.Enqueue(CreateTask("e2")); - - qm.Dispose(); - - Assert.Empty(qm.GetActiveTasks()); - } - - [Fact] - public void Dispose_多次调用不抛异常() - { - var qm = new DownloadQueueManager(); - qm.Dispose(); - // 第二次 Dispose 不应抛异常 - qm.Dispose(); - } - - // ===== 事件订阅测试 ===== - - [Fact] - public void 事件订阅和取消订阅() - { - using var qm = new DownloadQueueManager(); - int callCount = 0; - EventHandler handler = (_, _) => callCount++; - - qm.DownloadStatusChanged += handler; - qm.Enqueue(CreateTask("e1")); - Assert.Equal(1, callCount); - - qm.DownloadStatusChanged -= handler; - qm.Enqueue(CreateTask("e2")); - Assert.Equal(1, callCount); // 取消订阅后不再触发 - } - - // ===== 任务处理流程 ===== - - [Fact] - public async Task 任务入队后自动处理_状态变为UpdateSuccessful() - { - using var qm = new DownloadQueueManager(); - var task = CreateTask("ext-success"); - var completed = new TaskCompletionSource(); - - qm.DownloadStatusChanged += (_, e) => - { - if (e.Task.Extension.Id == "ext-success" && - e.Task.Status == ExtensionUpdateStatus.UpdateSuccessful) - { - completed.TrySetResult(true); - } - }; - - qm.Enqueue(task); - - // 等待任务完成_处理速度很快,因为ProcessTaskAsync只是Task.Delay(100) - var timeout = Task.Delay(5000); - var result = await Task.WhenAny(completed.Task, timeout); - Assert.Same(completed.Task, result); - Assert.True(completed.Task.Result); - Assert.Equal(ExtensionUpdateStatus.UpdateSuccessful, task.Status); - Assert.Equal(100, task.Progress); - } - - [Fact] - public async Task 入队任务被取消_状态变为UpdateFailed() - { - using var qm = new DownloadQueueManager(); - var task = CreateTask("ext-cancel"); - var resultTcs = new TaskCompletionSource(); - - qm.DownloadStatusChanged += (_, e) => - { - if (e.Task.Extension.Id == "ext-cancel" && - (e.Task.Status == ExtensionUpdateStatus.UpdateFailed || - e.Task.Status == ExtensionUpdateStatus.UpdateSuccessful)) - { - resultTcs.TrySetResult(e.Task.Status); - } - }; - - qm.Enqueue(task); - qm.CancelTask("ext-cancel"); - - var timeout = Task.Delay(5000); - var result = await Task.WhenAny(resultTcs.Task, timeout); - Assert.Same(resultTcs.Task, result); - Assert.Equal(ExtensionUpdateStatus.UpdateFailed, resultTcs.Task.Result); - } - - [Fact] - public void GetTask_返回任务的副本引用_非新对象() - { - using var qm = new DownloadQueueManager(); - var task = CreateTask(); - qm.Enqueue(task); - - var found = qm.GetTask("ext-1"); - Assert.Same(task, found); - } -} diff --git a/src/c#/ExtensionTest/ExtensionTest.csproj b/src/c#/ExtensionTest/ExtensionTest.csproj deleted file mode 100644 index dce49da5..00000000 --- a/src/c#/ExtensionTest/ExtensionTest.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net10.0 - enable - latest - enable - false - true - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - diff --git a/src/c#/BowlTest/BowlContextTests.cs b/tests/BowlTest/BowlContextTests.cs similarity index 100% rename from src/c#/BowlTest/BowlContextTests.cs rename to tests/BowlTest/BowlContextTests.cs diff --git a/src/c#/BowlTest/BowlResultTests.cs b/tests/BowlTest/BowlResultTests.cs similarity index 100% rename from src/c#/BowlTest/BowlResultTests.cs rename to tests/BowlTest/BowlResultTests.cs diff --git a/src/c#/BowlTest/BowlTests.cs b/tests/BowlTest/BowlTests.cs similarity index 100% rename from src/c#/BowlTest/BowlTests.cs rename to tests/BowlTest/BowlTests.cs diff --git a/src/c#/BowlTest/CrashInfoTests.cs b/tests/BowlTest/CrashInfoTests.cs similarity index 100% rename from src/c#/BowlTest/CrashInfoTests.cs rename to tests/BowlTest/CrashInfoTests.cs diff --git a/src/c#/BowlTest/DumpTypeTests.cs b/tests/BowlTest/DumpTypeTests.cs similarity index 100% rename from src/c#/BowlTest/DumpTypeTests.cs rename to tests/BowlTest/DumpTypeTests.cs diff --git a/tests/BowlTest/FileSystem/StorageHelperTests.cs b/tests/BowlTest/FileSystem/StorageHelperTests.cs new file mode 100644 index 00000000..6bd37e6c --- /dev/null +++ b/tests/BowlTest/FileSystem/StorageHelperTests.cs @@ -0,0 +1,180 @@ +using System.Text.Json; +using GeneralUpdate.Bowl.FileSystem; + +namespace BowlTest.FileSystem; + +/// +/// Unit tests for following AAAT pattern. +/// Tests directory operations, JSON creation, and edge cases. +/// +public class StorageHelperTests : IDisposable +{ + private readonly string _testBasePath; + + public StorageHelperTests() + { + _testBasePath = Path.Combine(Path.GetTempPath(), $"StorageHelperTest_{Guid.NewGuid()}"); + Directory.CreateDirectory(_testBasePath); + } + + public void Dispose() + { + if (Directory.Exists(_testBasePath)) + { + try { Directory.Delete(_testBasePath, recursive: true); } + catch { /* cleanup failure is non-fatal */ } + } + } + + #region CreateJson + + [Fact] + public void CreateJson_WritesValidJsonFile() + { + var filePath = Path.Combine(_testBasePath, "test.json"); + var obj = new { Name = "Test", Value = 42 }; + + StorageHelper.CreateJson(filePath, obj); + + Assert.True(System.IO.File.Exists(filePath)); + var content = System.IO.File.ReadAllText(filePath); + Assert.Contains("Test", content); + Assert.Contains("42", content); + } + + [Fact] + public void CreateJson_CreatesDirectoryIfMissing() + { + var subDir = Path.Combine(_testBasePath, "nested", "deep"); + var filePath = Path.Combine(subDir, "data.json"); + var obj = new { Key = "value" }; + + StorageHelper.CreateJson(filePath, obj); + + Assert.True(System.IO.File.Exists(filePath)); + } + + [Fact] + public void CreateJson_ProducesDeserializableJson() + { + var filePath = Path.Combine(_testBasePath, "roundtrip.json"); + var original = new TestModel { Id = 1, Name = "roundtrip" }; + + StorageHelper.CreateJson(filePath, original); + + var json = System.IO.File.ReadAllText(filePath); + var deserialized = JsonSerializer.Deserialize(json); + + Assert.NotNull(deserialized); + Assert.Equal(original.Id, deserialized.Id); + Assert.Equal(original.Name, deserialized.Name); + } + + #endregion + + #region Restore + + [Fact] + public void Restore_CopiesFilesFromBackupToSource() + { + var backupPath = Path.Combine(_testBasePath, "backup"); + var sourcePath = Path.Combine(_testBasePath, "source"); + Directory.CreateDirectory(backupPath); + System.IO.File.WriteAllText(Path.Combine(backupPath, "file1.txt"), "content1"); + System.IO.File.WriteAllText(Path.Combine(backupPath, "file2.txt"), "content2"); + + StorageHelper.Restore(backupPath, sourcePath); + + Assert.True(System.IO.File.Exists(Path.Combine(sourcePath, "file1.txt"))); + Assert.True(System.IO.File.Exists(Path.Combine(sourcePath, "file2.txt"))); + Assert.Equal("content1", System.IO.File.ReadAllText(Path.Combine(sourcePath, "file1.txt"))); + } + + [Fact] + public void Restore_CreatesSourceDirectoryIfMissing() + { + var backupPath = Path.Combine(_testBasePath, "backup2"); + var sourcePath = Path.Combine(_testBasePath, "new_source"); + Directory.CreateDirectory(backupPath); + System.IO.File.WriteAllText(Path.Combine(backupPath, "readme.txt"), "backup content"); + + StorageHelper.Restore(backupPath, sourcePath); + + Assert.True(Directory.Exists(sourcePath)); + Assert.True(System.IO.File.Exists(Path.Combine(sourcePath, "readme.txt"))); + } + + [Fact] + public void Restore_CopiesNestedDirectories() + { + var backupPath = Path.Combine(_testBasePath, "nested_backup"); + var sourcePath = Path.Combine(_testBasePath, "nested_source"); + var nestedDir = Path.Combine(backupPath, "subdir"); + Directory.CreateDirectory(nestedDir); + System.IO.File.WriteAllText(Path.Combine(nestedDir, "nested.txt"), "nested content"); + System.IO.File.WriteAllText(Path.Combine(backupPath, "root.txt"), "root content"); + + StorageHelper.Restore(backupPath, sourcePath); + + Assert.True(System.IO.File.Exists(Path.Combine(sourcePath, "root.txt"))); + Assert.True(System.IO.File.Exists(Path.Combine(sourcePath, "subdir", "nested.txt"))); + Assert.Equal("nested content", + System.IO.File.ReadAllText(Path.Combine(sourcePath, "subdir", "nested.txt"))); + } + + #endregion + + #region DeleteDirectory + + [Fact] + public void DeleteDirectory_RemovesDirectoryAndContents() + { + var targetDir = Path.Combine(_testBasePath, "to_delete"); + Directory.CreateDirectory(targetDir); + System.IO.File.WriteAllText(Path.Combine(targetDir, "temp.txt"), "temp"); + + StorageHelper.DeleteDirectory(targetDir); + + Assert.False(Directory.Exists(targetDir)); + } + + [Fact] + public void DeleteDirectory_RemovesNestedDirectories() + { + var targetDir = Path.Combine(_testBasePath, "nested_delete"); + var nestedDir = Path.Combine(targetDir, "inner"); + Directory.CreateDirectory(nestedDir); + System.IO.File.WriteAllText(Path.Combine(nestedDir, "file.txt"), "data"); + + StorageHelper.DeleteDirectory(targetDir); + + Assert.False(Directory.Exists(targetDir)); + } + + [Fact] + public void DeleteDirectory_RemovesReadOnlyFiles() + { + var targetDir = Path.Combine(_testBasePath, "readonly_delete"); + Directory.CreateDirectory(targetDir); + var filePath = Path.Combine(targetDir, "readonly.txt"); + System.IO.File.WriteAllText(filePath, "readonly"); + System.IO.File.SetAttributes(filePath, FileAttributes.ReadOnly); + + // Should not throw + StorageHelper.DeleteDirectory(targetDir); + + Assert.False(Directory.Exists(targetDir)); + } + + #endregion + + #region Helper model + + private sealed class TestModel + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + } + + #endregion +} diff --git a/src/c#/BowlTest/GlobalUsings.cs b/tests/BowlTest/GlobalUsings.cs similarity index 100% rename from src/c#/BowlTest/GlobalUsings.cs rename to tests/BowlTest/GlobalUsings.cs diff --git a/src/c#/BowlTest/Internal/CrashJsonContextTests.cs b/tests/BowlTest/Internal/CrashJsonContextTests.cs similarity index 100% rename from src/c#/BowlTest/Internal/CrashJsonContextTests.cs rename to tests/BowlTest/Internal/CrashJsonContextTests.cs diff --git a/src/c#/BowlTest/Internal/CrashReporterTests.cs b/tests/BowlTest/Internal/CrashReporterTests.cs similarity index 100% rename from src/c#/BowlTest/Internal/CrashReporterTests.cs rename to tests/BowlTest/Internal/CrashReporterTests.cs diff --git a/src/c#/BowlTest/Internal/CrashTests.cs b/tests/BowlTest/Internal/CrashTests.cs similarity index 100% rename from src/c#/BowlTest/Internal/CrashTests.cs rename to tests/BowlTest/Internal/CrashTests.cs diff --git a/src/c#/BowlTest/Internal/SystemInfoProviderFactoryTests.cs b/tests/BowlTest/Internal/SystemInfoProviderFactoryTests.cs similarity index 100% rename from src/c#/BowlTest/Internal/SystemInfoProviderFactoryTests.cs rename to tests/BowlTest/Internal/SystemInfoProviderFactoryTests.cs diff --git a/src/c#/BowlTest/Strategies/LinuxBowlStrategyTests.cs b/tests/BowlTest/Strategies/LinuxBowlStrategyTests.cs similarity index 100% rename from src/c#/BowlTest/Strategies/LinuxBowlStrategyTests.cs rename to tests/BowlTest/Strategies/LinuxBowlStrategyTests.cs diff --git a/src/c#/BowlTest/Strategies/ProcessExitResultTests.cs b/tests/BowlTest/Strategies/ProcessExitResultTests.cs similarity index 100% rename from src/c#/BowlTest/Strategies/ProcessExitResultTests.cs rename to tests/BowlTest/Strategies/ProcessExitResultTests.cs diff --git a/tests/BowlTest/Strategies/ProcessRunnerTests.cs b/tests/BowlTest/Strategies/ProcessRunnerTests.cs new file mode 100644 index 00000000..06ddb643 --- /dev/null +++ b/tests/BowlTest/Strategies/ProcessRunnerTests.cs @@ -0,0 +1,177 @@ +using System.Diagnostics; +using GeneralUpdate.Bowl.Strategies; + +namespace BowlTest.Strategies; + +/// +/// Unit tests for following AAAT pattern. +/// Tests process execution, output capture, timeout, and cancellation. +/// +public class ProcessRunnerTests +{ + #region RunAsync — successful execution + + [Fact] + public async Task RunAsync_SuccessfulCommand_ReturnsExitCodeZero() + { + var psi = new ProcessStartInfo + { + FileName = "cmd", + Arguments = "/c exit 0", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + var result = await ProcessRunner.RunAsync(psi, timeoutMs: 10000); + + Assert.Equal(0, result.ExitCode); + } + + [Fact] + public async Task RunAsync_NonZeroExitCode_ReturnsCorrectExitCode() + { + var psi = new ProcessStartInfo + { + FileName = "cmd", + Arguments = "/c exit 7", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + var result = await ProcessRunner.RunAsync(psi, timeoutMs: 10000); + + Assert.Equal(7, result.ExitCode); + } + + #endregion + + #region RunAsync — output capture + + [Fact] + public async Task RunAsync_CapturesStandardOutput() + { + var psi = new ProcessStartInfo + { + FileName = "cmd", + Arguments = "/c echo ProcessRunnerTest", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + var result = await ProcessRunner.RunAsync(psi, timeoutMs: 10000); + + Assert.Contains(result.OutputLines, line => line.Contains("ProcessRunnerTest")); + } + + [Fact] + public async Task RunAsync_OutputLines_IsNotNull() + { + var psi = new ProcessStartInfo + { + FileName = "cmd", + Arguments = "/c echo hello", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + var result = await ProcessRunner.RunAsync(psi, timeoutMs: 10000); + + Assert.NotNull(result.OutputLines); + } + + #endregion + + #region RunAsync — failed process start + + [Fact] + public async Task RunAsync_NonExistentCommand_ThrowsException() + { + var psi = new ProcessStartInfo + { + FileName = "nonexistent_command_xyz_123.exe", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + await Assert.ThrowsAnyAsync( + () => ProcessRunner.RunAsync(psi, timeoutMs: 5000)); + } + + #endregion + + #region RunAsync — timeout + + [Fact] + public async Task RunAsync_Timeout_ThrowsTimeoutException() + { + var psi = new ProcessStartInfo + { + FileName = "cmd", + Arguments = "/c ping -n 30 127.0.0.1 > nul", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + await Assert.ThrowsAsync( + () => ProcessRunner.RunAsync(psi, timeoutMs: 500)); + } + + #endregion + + #region RunAsync — cancellation + + [Fact] + public async Task RunAsync_Cancellation_ThrowsOperationCanceledException() + { + var psi = new ProcessStartInfo + { + FileName = "cmd", + Arguments = "/c ping -n 30 127.0.0.1 > nul", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var cts = new CancellationTokenSource(300); + + await Assert.ThrowsAnyAsync( + () => ProcessRunner.RunAsync(psi, timeoutMs: 30000, cts.Token)); + } + + #endregion + + #region RunAsync — structured result + + [Fact] + public async Task RunAsync_ResultHasOutputLines() + { + var psi = new ProcessStartInfo + { + FileName = "cmd", + Arguments = "/c echo line1 && echo line2", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + var result = await ProcessRunner.RunAsync(psi, timeoutMs: 10000); + + Assert.NotEmpty(result.OutputLines); + } + + #endregion +} diff --git a/src/c#/BowlTest/Strategies/StrategyFactoryTests.cs b/tests/BowlTest/Strategies/StrategyFactoryTests.cs similarity index 100% rename from src/c#/BowlTest/Strategies/StrategyFactoryTests.cs rename to tests/BowlTest/Strategies/StrategyFactoryTests.cs diff --git a/src/c#/BowlTest/Strategies/WindowsBowlStrategyTests.cs b/tests/BowlTest/Strategies/WindowsBowlStrategyTests.cs similarity index 100% rename from src/c#/BowlTest/Strategies/WindowsBowlStrategyTests.cs rename to tests/BowlTest/Strategies/WindowsBowlStrategyTests.cs diff --git a/src/c#/BowlTest/Utilities/TestFakes.cs b/tests/BowlTest/Utilities/TestFakes.cs similarity index 100% rename from src/c#/BowlTest/Utilities/TestFakes.cs rename to tests/BowlTest/Utilities/TestFakes.cs diff --git a/tests/CoreTest/Download/Executors/OssDownloadExecutorTests.cs b/tests/CoreTest/Download/Executors/OssDownloadExecutorTests.cs new file mode 100644 index 00000000..5898ba56 --- /dev/null +++ b/tests/CoreTest/Download/Executors/OssDownloadExecutorTests.cs @@ -0,0 +1,291 @@ +using System.Net; +using System.Text; +using GeneralUpdate.Core.Download.Executors; +using GeneralUpdate.Core.Download.Models; +using Moq; +using Moq.Protected; + +namespace CoreTest.Download.Executors; + +/// +/// Unit tests for following AAAT pattern. +/// Tests constructor validation, ExecuteAsync with mocked HTTP responses. +/// +public class OssDownloadExecutorTests +{ + private static DownloadAsset CreateAsset(string url = "http://example.com/pkg.zip") => new( + Name: "pkg.zip", + Url: url, + Size: 1024, + SHA256: "abc123", + Version: "1.0.0"); + + #region Constructor + + [Fact] + public void Ctor_WithValidHttpClient_CreatesInstance() + { + var client = new HttpClient(); + var executor = new OssDownloadExecutor(client); + Assert.NotNull(executor); + } + + [Fact] + public void Ctor_NullHttpClient_ThrowsArgumentNullException() + { + Assert.Throws(() => new OssDownloadExecutor(null!)); + } + + #endregion + + #region ExecuteAsync — successful download + + [Fact] + public async Task ExecuteAsync_SuccessfulDownload_ReturnsCompletedResult() + { + // Arrange + var payload = Encoding.UTF8.GetBytes("test-payload-content"); + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new ByteArrayContent(payload) + { + Headers = { ContentLength = payload.Length } + } + }); + + var client = new HttpClient(handler.Object); + var executor = new OssDownloadExecutor(client); + var asset = CreateAsset(); + var destPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.zip"); + + try + { + // Act + var result = await executor.ExecuteAsync(asset, destPath); + + // Assert + Assert.True(result.Success); + Assert.Equal(destPath, result.LocalPath); + Assert.True(File.Exists(destPath)); + Assert.Equal(payload.Length, result.DownloadedBytes); + } + finally + { + if (File.Exists(destPath)) File.Delete(destPath); + } + } + + #endregion + + #region ExecuteAsync — HTTP error + + [Fact] + public async Task ExecuteAsync_NonSuccessStatusCode_ReturnsFailedResult() + { + // Arrange + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.NotFound + }); + + var client = new HttpClient(handler.Object); + var executor = new OssDownloadExecutor(client); + var asset = CreateAsset(); + var destPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.zip"); + + try + { + // Act + var result = await executor.ExecuteAsync(asset, destPath); + + // Assert + Assert.False(result.Success); + Assert.NotNull(result.ErrorMessage); + } + finally + { + if (File.Exists(destPath)) File.Delete(destPath); + } + } + + #endregion + + #region ExecuteAsync — progress reporting + + [Fact] + public async Task ExecuteAsync_WithProgress_Reports100Percent() + { + // Arrange + var payload = Encoding.UTF8.GetBytes("test-content"); + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new ByteArrayContent(payload) + { + Headers = { ContentLength = payload.Length } + } + }); + + var client = new HttpClient(handler.Object); + var executor = new OssDownloadExecutor(client); + var asset = CreateAsset(); + var destPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.zip"); + + var progressValues = new List(); + var progress = new Progress(p => progressValues.Add(p)); + + try + { + // Act + var result = await executor.ExecuteAsync(asset, destPath, progress); + + // Assert + Assert.True(result.Success); + Assert.Contains(progressValues, p => p.Percentage == 100); + } + finally + { + if (File.Exists(destPath)) File.Delete(destPath); + } + } + + #endregion + + #region ExecuteAsync — directory creation + + [Fact] + public async Task ExecuteAsync_CreatesDestinationDirectory() + { + // Arrange + var payload = Encoding.UTF8.GetBytes("content"); + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new ByteArrayContent(payload) + }); + + var client = new HttpClient(handler.Object); + var executor = new OssDownloadExecutor(client); + var asset = CreateAsset(); + + var subDir = Path.Combine(Path.GetTempPath(), $"oss_test_{Guid.NewGuid()}"); + var destPath = Path.Combine(subDir, "nested", "file.zip"); + + try + { + // Act + var result = await executor.ExecuteAsync(asset, destPath); + + // Assert + Assert.True(result.Success); + Assert.True(Directory.Exists(Path.GetDirectoryName(destPath))); + Assert.True(File.Exists(destPath)); + } + finally + { + if (Directory.Exists(subDir)) + Directory.Delete(subDir, recursive: true); + } + } + + #endregion + + #region ExecuteAsync — execution time recorded + + [Fact] + public async Task ExecuteAsync_RecordsDuration() + { + // Arrange + var payload = Encoding.UTF8.GetBytes("data"); + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new ByteArrayContent(payload) + }); + + var client = new HttpClient(handler.Object); + var executor = new OssDownloadExecutor(client); + var asset = CreateAsset(); + var destPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.zip"); + + try + { + // Act + var result = await executor.ExecuteAsync(asset, destPath); + + // Assert + Assert.True(result.Duration > TimeSpan.Zero); + } + finally + { + if (File.Exists(destPath)) File.Delete(destPath); + } + } + + #endregion + + #region ExecuteAsync — null progress does not throw + + [Fact] + public async Task ExecuteAsync_NullProgress_DoesNotThrow() + { + // Arrange + var payload = Encoding.UTF8.GetBytes("data"); + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new ByteArrayContent(payload) + }); + + var client = new HttpClient(handler.Object); + var executor = new OssDownloadExecutor(client); + var asset = CreateAsset(); + var destPath = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}.zip"); + + try + { + // Act + var result = await executor.ExecuteAsync(asset, destPath, null); + + // Assert + Assert.True(result.Success); + } + finally + { + if (File.Exists(destPath)) File.Delete(destPath); + } + } + + #endregion +} diff --git a/tests/CoreTest/Download/Sources/HttpDownloadSourceTests.cs b/tests/CoreTest/Download/Sources/HttpDownloadSourceTests.cs new file mode 100644 index 00000000..7a620015 --- /dev/null +++ b/tests/CoreTest/Download/Sources/HttpDownloadSourceTests.cs @@ -0,0 +1,98 @@ +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Sources; + +namespace CoreTest.Download.Sources; + +/// +/// Unit tests for following AAAT pattern. +/// Tests constructor validation, interface implementation, and basic behaviour. +/// +public class HttpDownloadSourceTests +{ + private const string ValidUrl = "http://localhost:5000/api/versions"; + private const string ClientVersion = "1.0.0"; + private const string AppSecretKey = "test-secret-key"; + + #region Constructor — valid inputs + + [Fact] + public void Ctor_WithAllRequiredParams_CreatesInstance() + { + var source = new HttpDownloadSource( + ValidUrl, ClientVersion, null, AppSecretKey, + PlatformType.Windows, null, null, null); + + Assert.NotNull(source); + } + + [Fact] + public void Ctor_WithAllOptionalParams_CreatesInstance() + { + var source = new HttpDownloadSource( + ValidUrl, ClientVersion, "2.0.0", AppSecretKey, + PlatformType.Linux, "product-1", "Bearer", "token-abc"); + + Assert.NotNull(source); + } + + [Fact] + public void Ctor_WithUpgradeClientVersion_CreatesInstance() + { + var source = new HttpDownloadSource( + ValidUrl, ClientVersion, "1.5.0", AppSecretKey, + PlatformType.Windows, null, null, null); + + Assert.NotNull(source); + } + + [Fact] + public void Ctor_WithMacOSPlatform_CreatesInstance() + { + var source = new HttpDownloadSource( + ValidUrl, ClientVersion, null, AppSecretKey, + PlatformType.MacOS, null, null, null); + + Assert.NotNull(source); + } + + #endregion + + #region Constructor — edge cases + + [Fact] + public void Ctor_WithEmptyClientVersion_CreatesInstance() + { + var source = new HttpDownloadSource( + ValidUrl, string.Empty, null, AppSecretKey, + PlatformType.Windows, null, null, null); + + Assert.NotNull(source); + } + + [Fact] + public void Ctor_WithEmptyProductId_CreatesInstance() + { + var source = new HttpDownloadSource( + ValidUrl, ClientVersion, null, AppSecretKey, + PlatformType.Windows, string.Empty, null, null); + + Assert.NotNull(source); + } + + #endregion + + #region IDownloadSource contract + + [Fact] + public void Implements_IDownloadSource() + { + var source = new HttpDownloadSource( + ValidUrl, ClientVersion, null, AppSecretKey, + PlatformType.Windows, null, null, null); + + Assert.IsAssignableFrom(source); + } + + #endregion +} diff --git a/tests/CoreTest/Download/Sources/OssDownloadSourceTests.cs b/tests/CoreTest/Download/Sources/OssDownloadSourceTests.cs new file mode 100644 index 00000000..a7e6bee0 --- /dev/null +++ b/tests/CoreTest/Download/Sources/OssDownloadSourceTests.cs @@ -0,0 +1,373 @@ +using System.Net; +using System.Text.Json; +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Download.Abstractions; +using GeneralUpdate.Core.Download.Models; +using GeneralUpdate.Core.Download.Sources; +using GeneralUpdate.Core.JsonContext; +using Moq; +using Moq.Protected; + +namespace CoreTest.Download.Sources; + +/// +/// Unit tests for following AAAT pattern. +/// Tests constructor validation, ListAsync with mocked HTTP responses. +/// +public class OssDownloadSourceTests +{ + private const string ValidUrl = "http://localhost:5000/versions.json"; + + #region Constructor + + [Fact] + public void Ctor_WithValidParams_CreatesInstance() + { + var client = new HttpClient(); + var source = new OssDownloadSource(client, ValidUrl); + + Assert.NotNull(source); + } + + [Fact] + public void Ctor_WithCustomTimeout_CreatesInstance() + { + var client = new HttpClient(); + var source = new OssDownloadSource(client, ValidUrl, TimeSpan.FromSeconds(30)); + + Assert.NotNull(source); + } + + [Fact] + public void Ctor_NullHttpClient_ThrowsArgumentNullException() + { + Assert.Throws(() => new OssDownloadSource(null!, ValidUrl)); + } + + [Fact] + public void Ctor_NullVersionJsonUrl_ThrowsArgumentNullException() + { + var client = new HttpClient(); + Assert.Throws(() => new OssDownloadSource(client, null!)); + } + + [Fact] + public void Ctor_DefaultTimeout_Is60Seconds() + { + var client = new HttpClient(); + var source = new OssDownloadSource(client, ValidUrl); + + Assert.NotNull(source); + } + + #endregion + + #region ListAsync — empty response + + [Fact] + public async Task ListAsync_EmptyVersionList_ReturnsEmptyAssets() + { + // Arrange + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("[]") + }); + + var client = new HttpClient(handler.Object); + var source = new OssDownloadSource(client, ValidUrl); + + // Act + var result = await source.ListAsync(); + + // Assert + Assert.NotNull(result); + Assert.Empty(result.Assets); + Assert.False(result.HasMainUpdate); + Assert.False(result.HasUpgradeUpdate); + } + + #endregion + + #region ListAsync — valid version list + + [Fact] + public async Task ListAsync_ValidVersionList_ReturnsAssetsSortedByPubTime() + { + // Arrange + var records = new List + { + new() + { + PacketName = "app-v2", + Version = "2.0.0", + Url = "http://example.com/app-v2.zip", + Hash = "abc123", + PubTime = new DateTime(2025, 6, 1) + }, + new() + { + PacketName = "app-v1", + Version = "1.0.0", + Url = "http://example.com/app-v1.zip", + Hash = "def456", + PubTime = new DateTime(2025, 1, 1) + } + }; + + var json = JsonSerializer.Serialize(records, OssVersionRecordJsonContext.Default.ListOssVersionRecord); + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(json) + }); + + var client = new HttpClient(handler.Object); + var source = new OssDownloadSource(client, ValidUrl); + + // Act + var result = await source.ListAsync(); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Assets.Count); + Assert.True(result.HasMainUpdate); + Assert.True(result.HasUpgradeUpdate); + // Should be sorted by PubTime ascending: v1 first, v2 second + Assert.Equal("1.0.0", result.Assets[0].Version); + Assert.Equal("2.0.0", result.Assets[1].Version); + Assert.Equal("app-v1.zip", result.Assets[0].Name); + Assert.Equal("app-v2.zip", result.Assets[1].Name); + } + + #endregion + + #region ListAsync — single version + + [Fact] + public async Task ListAsync_SingleVersion_ReturnsOneAsset() + { + // Arrange + var records = new List + { + new() + { + PacketName = "app", + Version = "1.0.0", + Url = "http://example.com/app.zip", + Hash = "hash123", + PubTime = new DateTime(2025, 1, 1) + } + }; + + var json = JsonSerializer.Serialize(records, OssVersionRecordJsonContext.Default.ListOssVersionRecord); + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(json) + }); + + var client = new HttpClient(handler.Object); + var source = new OssDownloadSource(client, ValidUrl); + + // Act + var result = await source.ListAsync(); + + // Assert + Assert.Single(result.Assets); + Assert.Equal("1.0.0", result.Assets[0].Version); + Assert.True(result.HasMainUpdate); + } + + #endregion + + #region ListAsync — HTTP error + + [Fact] + public async Task ListAsync_NonSuccessStatusCode_ThrowsHttpRequestException() + { + // Arrange + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.InternalServerError + }); + + var client = new HttpClient(handler.Object); + var source = new OssDownloadSource(client, ValidUrl); + + // Act & Assert + await Assert.ThrowsAsync(() => source.ListAsync()); + } + + #endregion + + #region ListAsync — version with null URL throws + + [Fact] + public async Task ListAsync_VersionWithNullUrl_ThrowsInvalidOperationException() + { + // Arrange + var records = new List + { + new() + { + PacketName = "app", + Version = "1.0.0", + Url = null, + Hash = "hash123", + PubTime = new DateTime(2025, 1, 1) + } + }; + + var json = JsonSerializer.Serialize(records, OssVersionRecordJsonContext.Default.ListOssVersionRecord); + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(json) + }); + + var client = new HttpClient(handler.Object); + var source = new OssDownloadSource(client, ValidUrl); + + // Act & Assert + await Assert.ThrowsAsync(() => source.ListAsync()); + } + + #endregion + + #region ListAsync — version with PacketName null uses Version + + [Fact] + public async Task ListAsync_PacketNameNull_UsesVersionForZipName() + { + // Arrange + var records = new List + { + new() + { + PacketName = null, + Version = "3.0.0", + Url = "http://example.com/pkg.zip", + Hash = "hash", + PubTime = new DateTime(2025, 1, 1) + } + }; + + var json = JsonSerializer.Serialize(records, OssVersionRecordJsonContext.Default.ListOssVersionRecord); + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(json) + }); + + var client = new HttpClient(handler.Object); + var source = new OssDownloadSource(client, ValidUrl); + + // Act + var result = await source.ListAsync(); + + // Assert + Assert.Single(result.Assets); + Assert.Equal("3.0.0.zip", result.Assets[0].Name); + } + + #endregion + + #region IDownloadSource contract + + [Fact] + public void Implements_IDownloadSource() + { + var client = new HttpClient(); + var source = new OssDownloadSource(client, ValidUrl); + + Assert.IsAssignableFrom(source); + } + + #endregion + + #region ListAsync — multiple versions with same URL + + [Fact] + public async Task ListAsync_SameUrlDifferentVersions_ReturnsAllVersions() + { + // Arrange — OssDownloadSource preserves all records (dedup is done by HttpDownloadSource, not here) + var records = new List + { + new() + { + PacketName = "pkg-a", Version = "1.0.0", + Url = "http://example.com/same.zip", Hash = "aaa", + PubTime = new DateTime(2025, 1, 1) + }, + new() + { + PacketName = "pkg-b", Version = "2.0.0", + Url = "http://example.com/same.zip", Hash = "bbb", + PubTime = new DateTime(2025, 6, 1) + }, + new() + { + PacketName = "pkg-c", Version = "3.0.0", + Url = "http://example.com/other.zip", Hash = "ccc", + PubTime = new DateTime(2025, 3, 1) + } + }; + + var json = JsonSerializer.Serialize(records, OssVersionRecordJsonContext.Default.ListOssVersionRecord); + var handler = new Mock(); + handler.Protected() + .Setup>("SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(json) + }); + + var client = new HttpClient(handler.Object); + var source = new OssDownloadSource(client, ValidUrl); + + // Act + var result = await source.ListAsync(); + + // Assert — OssDownloadSource returns all records, sorted by PubTime asc + Assert.Equal(3, result.Assets.Count); + Assert.Equal("1.0.0", result.Assets[0].Version); + Assert.Equal("3.0.0", result.Assets[1].Version); + Assert.Equal("2.0.0", result.Assets[2].Version); + } + + #endregion +} diff --git a/tests/CoreTest/Hubs/UpgradeHubServiceTests.cs b/tests/CoreTest/Hubs/UpgradeHubServiceTests.cs new file mode 100644 index 00000000..03468e61 --- /dev/null +++ b/tests/CoreTest/Hubs/UpgradeHubServiceTests.cs @@ -0,0 +1,139 @@ +using GeneralUpdate.Core.Hubs; + +namespace CoreTest.Hubs; + +/// +/// Unit tests for following AAAT pattern. +/// Tests the SignalR hub connection builder, listener registration, and lifecycle methods. +/// +public class UpgradeHubServiceTests +{ + private const string ValidUrl = "http://localhost:5000/UpgradeHub"; + + #region Constructor + + [Fact] + public void Ctor_WithValidUrl_CreatesInstance() + { + var service = new UpgradeHubService(ValidUrl); + Assert.NotNull(service); + } + + [Fact] + public void Ctor_WithToken_CreatesInstance() + { + var service = new UpgradeHubService(ValidUrl, token: "test-token"); + Assert.NotNull(service); + } + + [Fact] + public void Ctor_WithAppKey_CreatesInstance() + { + var service = new UpgradeHubService(ValidUrl, appkey: "test-app-key"); + Assert.NotNull(service); + } + + [Fact] + public void Ctor_WithTokenAndAppKey_CreatesInstance() + { + var service = new UpgradeHubService(ValidUrl, token: "test-token", appkey: "test-app-key"); + Assert.NotNull(service); + } + + [Fact] + public void Ctor_WithNullToken_DoesNotThrow() + { + var service = new UpgradeHubService(ValidUrl, token: null); + Assert.NotNull(service); + } + + [Fact] + public void Ctor_WithNullAppKey_DoesNotThrow() + { + var service = new UpgradeHubService(ValidUrl, appkey: null); + Assert.NotNull(service); + } + + #endregion + + #region AddListenerReceive + + [Fact] + public void AddListenerReceive_WithValidCallback_DoesNotThrow() + { + var service = new UpgradeHubService(ValidUrl); + var received = false; + service.AddListenerReceive(_ => received = true); + // Listener registered without exception + } + + [Fact] + public void AddListenerReceive_MultipleCallbacks_DoesNotThrow() + { + var service = new UpgradeHubService(ValidUrl); + service.AddListenerReceive(_ => { }); + service.AddListenerReceive(_ => { }); + } + + #endregion + + #region AddListenerOnline + + [Fact] + public void AddListenerOnline_WithValidCallback_DoesNotThrow() + { + var service = new UpgradeHubService(ValidUrl); + var online = false; + service.AddListenerOnline(_ => online = true); + } + + [Fact] + public void AddListenerOnline_MultipleCallbacks_DoesNotThrow() + { + var service = new UpgradeHubService(ValidUrl); + service.AddListenerOnline(_ => { }); + service.AddListenerOnline(_ => { }); + } + + #endregion + + #region AddListenerReconnected + + [Fact] + public void AddListenerReconnected_WithValidCallback_DoesNotThrow() + { + var service = new UpgradeHubService(ValidUrl); + service.AddListenerReconnected(_ => Task.CompletedTask); + } + + [Fact] + public void AddListenerReconnected_WithNullCallback_DoesNotThrow() + { + var service = new UpgradeHubService(ValidUrl); + service.AddListenerReconnected(null); + } + + #endregion + + #region AddListenerClosed + + [Fact] + public void AddListenerClosed_WithValidCallback_DoesNotThrow() + { + var service = new UpgradeHubService(ValidUrl); + service.AddListenerClosed(_ => Task.CompletedTask); + } + + #endregion + + #region IUpgradeHubService contract + + [Fact] + public void Implements_IUpgradeHubService() + { + var service = new UpgradeHubService(ValidUrl); + Assert.IsAssignableFrom(service); + } + + #endregion +} diff --git a/tests/CoreTest/Strategy/OssStrategyTests.cs b/tests/CoreTest/Strategy/OssStrategyTests.cs new file mode 100644 index 00000000..2f221596 --- /dev/null +++ b/tests/CoreTest/Strategy/OssStrategyTests.cs @@ -0,0 +1,172 @@ +using GeneralUpdate.Core.Configuration; +using GeneralUpdate.Core.Hooks; +using GeneralUpdate.Core.Strategy; +using Moq; + +namespace CoreTest.Strategy; + +/// +/// Unit tests for following AAAT pattern. +/// Tests constructor, Create, interface implementation, and hook/reporter wiring. +/// +public class OssStrategyTests +{ + #region Constructor + + [Fact] + public void Ctor_WithOssClientRole_CreatesInstance() + { + var strategy = new OssStrategy(AppType.OssClient); + Assert.NotNull(strategy); + } + + [Fact] + public void Ctor_WithOssUpgradeRole_CreatesInstance() + { + var strategy = new OssStrategy(AppType.OssUpgrade); + Assert.NotNull(strategy); + } + + [Fact] + public void Ctor_DefaultsToIStrategy() + { + var strategy = new OssStrategy(AppType.OssClient); + Assert.IsAssignableFrom(strategy); + } + + #endregion + + #region Create + + [Fact] + public void Create_WithValidContext_DoesNotThrow() + { + var strategy = new OssStrategy(AppType.OssClient); + var context = new UpdateContext + { + InstallPath = "C:\\test", + ClientVersion = "1.0.0", + UpdateUrl = "http://localhost/versions.json" + }; + + strategy.Create(context); + } + + [Fact] + public void Create_WithNullContext_ThrowsArgumentNullException() + { + var strategy = new OssStrategy(AppType.OssClient); + Assert.Throws(() => strategy.Create(null!)); + } + + #endregion + + #region ExecuteAsync — not configured + + [Fact] + public async Task ExecuteAsync_WithoutCreate_ThrowsInvalidOperationException() + { + var strategy = new OssStrategy(AppType.OssClient); + + await Assert.ThrowsAsync(() => strategy.ExecuteAsync()); + } + + #endregion + + #region Hooks property + + [Fact] + public void Hooks_DefaultValue_IsNoOpUpdateHooks() + { + var strategy = new OssStrategy(AppType.OssClient); + Assert.NotNull(strategy.Hooks); + Assert.IsType(strategy.Hooks); + } + + [Fact] + public void Hooks_CanBeSet_CustomImplementation() + { + var strategy = new OssStrategy(AppType.OssClient); + var mockHooks = new Mock(); + + strategy.Hooks = mockHooks.Object; + + Assert.Same(mockHooks.Object, strategy.Hooks); + } + + #endregion + + #region DownloadSource property + + [Fact] + public void DownloadSource_DefaultValue_IsNull() + { + var strategy = new OssStrategy(AppType.OssClient); + Assert.Null(strategy.DownloadSource); + } + + [Fact] + public void DownloadSource_CanBeSet_ToCustomSource() + { + var strategy = new OssStrategy(AppType.OssClient); + var mockSource = new Mock(); + + strategy.DownloadSource = mockSource.Object; + + Assert.Same(mockSource.Object, strategy.DownloadSource); + } + + #endregion + + #region DownloadOrchestrator property + + [Fact] + public void DownloadOrchestrator_DefaultValue_IsNull() + { + var strategy = new OssStrategy(AppType.OssClient); + Assert.Null(strategy.DownloadOrchestrator); + } + + [Fact] + public void DownloadOrchestrator_CanBeSet_ToCustomOrchestrator() + { + var strategy = new OssStrategy(AppType.OssClient); + var mockOrchestrator = new Mock(); + + strategy.DownloadOrchestrator = mockOrchestrator.Object; + + Assert.Same(mockOrchestrator.Object, strategy.DownloadOrchestrator); + } + + #endregion + + #region StartAppAsync + + [Fact] + public async Task StartAppAsync_WithoutConfig_CompletesWithoutException() + { + var strategy = new OssStrategy(AppType.OssClient); + + // Should not throw — returns completed task when config is null + await strategy.StartAppAsync(); + } + + #endregion + + #region Multiple Create calls + + [Fact] + public void Create_SecondCall_OverwritesConfig() + { + var strategy = new OssStrategy(AppType.OssClient); + var context1 = new UpdateContext { ClientVersion = "1.0.0" }; + var context2 = new UpdateContext { ClientVersion = "2.0.0" }; + + strategy.Create(context1); + strategy.Create(context2); + + // No exception — second Create overwrites the first + } + + #endregion +} diff --git a/tests/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs b/tests/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs new file mode 100644 index 00000000..5b9187c9 --- /dev/null +++ b/tests/DifferentialTest/Pipeline/DiffPipelineOptionsTests.cs @@ -0,0 +1,49 @@ +using GeneralUpdate.Core.Pipeline; + +namespace DifferentialTest.Pipeline +{ + /// + /// Unit tests for . + /// + public class DiffPipelineOptionsTests + { + [Fact] + public void DefaultConstructor_MaxDegreeOfParallelism_Equals2() + { + var options = new DiffPipelineOptions(); + Assert.Equal(2, options.MaxDegreeOfParallelism); + } + + [Fact] + public void DefaultConstructor_StopOnFirstError_IsFalse() + { + var options = new DiffPipelineOptions(); + Assert.False(options.StopOnFirstError); + } + + [Fact] + public void MaxDegreeOfParallelism_Set_ReturnsNewValue() + { + var options = new DiffPipelineOptions { MaxDegreeOfParallelism = 8 }; + Assert.Equal(8, options.MaxDegreeOfParallelism); + } + + [Fact] + public void StopOnFirstError_Set_ReturnsNewValue() + { + var options = new DiffPipelineOptions { StopOnFirstError = true }; + Assert.True(options.StopOnFirstError); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(64)] + [InlineData(int.MaxValue)] + public void MaxDegreeOfParallelism_BoundaryValues_SetsCorrectly(int value) + { + var options = new DiffPipelineOptions { MaxDegreeOfParallelism = value }; + Assert.Equal(value, options.MaxDegreeOfParallelism); + } + } +} diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index 717c6de7..f992507e 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -8,5 +8,6 @@ + diff --git a/src/c#/DrivelutionTest/Core/DrivelutionOptionsTests.cs b/tests/DrivelutionTest/Core/DrivelutionOptionsTests.cs similarity index 100% rename from src/c#/DrivelutionTest/Core/DrivelutionOptionsTests.cs rename to tests/DrivelutionTest/Core/DrivelutionOptionsTests.cs diff --git a/tests/DrivelutionTest/Core/ServiceCollectionExtensionsTests.cs b/tests/DrivelutionTest/Core/ServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..a93c1f22 --- /dev/null +++ b/tests/DrivelutionTest/Core/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,128 @@ +using GeneralUpdate.Drivelution.Abstractions; +using GeneralUpdate.Drivelution.Abstractions.Configuration; +using GeneralUpdate.Drivelution.Core; +using GeneralUpdate.Drivelution.Core.Execution; +using Microsoft.Extensions.DependencyInjection; + +namespace DrivelutionTest.Core; + +/// +/// Unit tests for following AAAT pattern. +/// Tests the DI registration of Drivelution services. +/// +public class ServiceCollectionExtensionsTests +{ + #region AddDrivelution — basic registration + + [Fact] + public void AddDrivelution_RegistersDrivelutionOptions() + { + var services = new ServiceCollection(); + + services.AddDrivelution(); + + var provider = services.BuildServiceProvider(); + var options = provider.GetService(); + Assert.NotNull(options); + } + + [Fact] + public void AddDrivelution_RegistersCommandRunner() + { + var services = new ServiceCollection(); + + services.AddDrivelution(); + + var provider = services.BuildServiceProvider(); + var runner = provider.GetService(); + Assert.NotNull(runner); + Assert.IsType(runner); + } + + [Fact] + public void AddDrivelution_RegistersPlatformServices() + { + var services = new ServiceCollection(); + + services.AddDrivelution(); + + var provider = services.BuildServiceProvider(); + + // At least one platform's services should be registered + var validator = provider.GetService(); + var backup = provider.GetService(); + var updater = provider.GetService(); + + Assert.NotNull(validator); + Assert.NotNull(backup); + Assert.NotNull(updater); + } + + #endregion + + #region AddDrivelution — with options configuration + + [Fact] + public void AddDrivelution_WithConfigure_UsesCustomOptions() + { + var services = new ServiceCollection(); + + services.AddDrivelution(options => + { + options.DefaultRetryCount = 7; + options.DefaultTimeoutSeconds = 999; + }); + + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService(); + + Assert.Equal(7, options.DefaultRetryCount); + Assert.Equal(999, options.DefaultTimeoutSeconds); + } + + #endregion + + #region AddDrivelution — returns service collection for chaining + + [Fact] + public void AddDrivelution_ReturnsSameServiceCollection() + { + var services = new ServiceCollection(); + + var result = services.AddDrivelution(); + + Assert.Same(services, result); + } + + #endregion + + #region AddDrivelution — services are singleton + + [Fact] + public void AddDrivelution_CommandRunner_IsSingleton() + { + var services = new ServiceCollection(); + services.AddDrivelution(); + var provider = services.BuildServiceProvider(); + + var runner1 = provider.GetRequiredService(); + var runner2 = provider.GetRequiredService(); + + Assert.Same(runner1, runner2); + } + + [Fact] + public void AddDrivelution_DrivelutionOptions_IsSingleton() + { + var services = new ServiceCollection(); + services.AddDrivelution(); + var provider = services.BuildServiceProvider(); + + var opt1 = provider.GetRequiredService(); + var opt2 = provider.GetRequiredService(); + + Assert.Same(opt1, opt2); + } + + #endregion +} diff --git a/tests/DrivelutionTest/DrivelutionTest.csproj b/tests/DrivelutionTest/DrivelutionTest.csproj index fe7667f7..d2ba0c75 100644 --- a/tests/DrivelutionTest/DrivelutionTest.csproj +++ b/tests/DrivelutionTest/DrivelutionTest.csproj @@ -15,6 +15,7 @@ + diff --git a/src/c#/DrivelutionTest/Exceptions/DriverUpdateExceptionsTests.cs b/tests/DrivelutionTest/Exceptions/DriverUpdateExceptionsTests.cs similarity index 100% rename from src/c#/DrivelutionTest/Exceptions/DriverUpdateExceptionsTests.cs rename to tests/DrivelutionTest/Exceptions/DriverUpdateExceptionsTests.cs diff --git a/tests/DrivelutionTest/Execution/CommandRunnerTests.cs b/tests/DrivelutionTest/Execution/CommandRunnerTests.cs new file mode 100644 index 00000000..97d27797 --- /dev/null +++ b/tests/DrivelutionTest/Execution/CommandRunnerTests.cs @@ -0,0 +1,140 @@ +using GeneralUpdate.Drivelution.Core.Execution; + +namespace DrivelutionTest.Execution; + +/// +/// Unit tests for following AAAT pattern. +/// Tests constructor, interface contract, and RunOrThrowAsync behaviour. +/// +public class CommandRunnerTests +{ + #region Constructor + + [Fact] + public void Ctor_CreatesInstance() + { + var runner = new CommandRunner(); + Assert.NotNull(runner); + } + + #endregion + + #region ICommandRunner contract + + [Fact] + public void Implements_ICommandRunner() + { + var runner = new CommandRunner(); + Assert.IsAssignableFrom(runner); + } + + #endregion + + #region RunAsync — basic execution + + [Fact] + public async Task RunAsync_SuccessfulCommand_ReturnsSuccessResult() + { + var runner = new CommandRunner(); + + // Use a simple built-in command that always succeeds + var result = await runner.RunAsync("cmd", new[] { "/c", "exit 0" }); + + Assert.NotNull(result); + Assert.Equal(0, result.ExitCode); + Assert.True(result.Success); + } + + [Fact] + public async Task RunAsync_FailingCommand_ReturnsNonZeroExitCode() + { + var runner = new CommandRunner(); + + var result = await runner.RunAsync("cmd", new[] { "/c", "exit 42" }); + + Assert.NotNull(result); + Assert.Equal(42, result.ExitCode); + Assert.False(result.Success); + } + + [Fact] + public async Task RunAsync_CapturesStandardOutput() + { + var runner = new CommandRunner(); + + var result = await runner.RunAsync("cmd", new[] { "/c", "echo HelloWorld" }); + + Assert.Contains("HelloWorld", result.StandardOutput); + } + + [Fact] + public async Task RunAsync_WithEmptyArguments_DoesNotThrow() + { + var runner = new CommandRunner(); + + var result = await runner.RunAsync("cmd", new[] { "/c", "exit 0" }); + + Assert.True(result.Success); + } + + #endregion + + #region RunOrThrowAsync + + [Fact] + public async Task RunOrThrowAsync_SuccessfulCommand_ReturnsResult() + { + var runner = new CommandRunner(); + + var result = await runner.RunOrThrowAsync("cmd", new[] { "/c", "exit 0" }); + + Assert.True(result.Success); + Assert.Equal(0, result.ExitCode); + } + + [Fact] + public async Task RunOrThrowAsync_FailingCommand_ThrowsInvalidOperationException() + { + var runner = new CommandRunner(); + + var ex = await Assert.ThrowsAsync( + () => runner.RunOrThrowAsync("cmd", new[] { "/c", "exit 1" })); + + Assert.Contains("failed with exit code 1", ex.Message); + } + + #endregion + + #region RunAsync — cancellation + + [Fact] + public async Task RunAsync_WithCancellation_PreemptsExecution() + { + var runner = new CommandRunner(); + using var cts = new CancellationTokenSource(); + + // Cancel after a short delay so the process has time to start + cts.CancelAfter(50); + + await Assert.ThrowsAnyAsync( + () => runner.RunAsync("cmd", new[] { "/c", "ping -n 10 127.0.0.1 > nul" }, cts.Token)); + } + + #endregion + + #region RunAsync — returns structured result + + [Fact] + public async Task RunAsync_ResultProperties_ArePopulated() + { + var runner = new CommandRunner(); + + var result = await runner.RunAsync("cmd", new[] { "/c", "echo test-output" }); + + Assert.NotNull(result.StandardOutput); + Assert.NotNull(result.StandardError); + Assert.True(result.ExitCode >= 0); + } + + #endregion +} diff --git a/tests/DrivelutionTest/Linux/Implementation/LinuxDriverValidatorTests.cs b/tests/DrivelutionTest/Linux/Implementation/LinuxDriverValidatorTests.cs new file mode 100644 index 00000000..78d28f0f --- /dev/null +++ b/tests/DrivelutionTest/Linux/Implementation/LinuxDriverValidatorTests.cs @@ -0,0 +1,80 @@ +using GeneralUpdate.Drivelution.Abstractions; +using GeneralUpdate.Drivelution.Linux.Implementation; + +namespace DrivelutionTest.Linux.Implementation; + +/// +/// Unit tests for following AAAT pattern. +/// Tests constructor and interface contract. +/// +public class LinuxDriverValidatorTests +{ + #region Constructor + + [Fact] + public void Ctor_CreatesInstance() + { + var validator = new LinuxDriverValidator(); + Assert.NotNull(validator); + } + + #endregion + + #region IDriverValidator contract + + [Fact] + public void Implements_IDriverValidator() + { + var validator = new LinuxDriverValidator(); + Assert.IsAssignableFrom(validator); + } + + #endregion + + #region ValidateCompatibilityAsync — with invalid path + + [Fact] + public async Task ValidateCompatibilityAsync_NullDriverInfo_ThrowsNullReferenceException() + { + var validator = new LinuxDriverValidator(); + + // CompatibilityChecker will throw when driverInfo is null + await Assert.ThrowsAnyAsync( + () => validator.ValidateCompatibilityAsync(null!)); + } + + #endregion + + #region ValidateIntegrityAsync — with non-existent file + + [Fact] + public async Task ValidateIntegrityAsync_NonExistentFile_ThrowsException() + { + var validator = new LinuxDriverValidator(); + + // HashValidator will throw when file doesn't exist + await Assert.ThrowsAnyAsync( + () => validator.ValidateIntegrityAsync( + "/nonexistent/path/driver.ko", "fake-hash")); + } + + #endregion + + #region ValidateSignatureAsync — with empty trusted publishers + + [Fact] + public async Task ValidateSignatureAsync_NoSignatureFile_ReturnsTrueIfNoPublishers() + { + var validator = new LinuxDriverValidator(); + + // Non-existent file has no .sig/.asc companion → returns !trustedPublishers.Any() + // When trustedPublishers is empty, returns true + var result = await validator.ValidateSignatureAsync( + "/nonexistent/path/driver.ko", + Array.Empty()); + + Assert.True(result); + } + + #endregion +} diff --git a/tests/DrivelutionTest/Linux/Implementation/LinuxGeneralDrivelutionTests.cs b/tests/DrivelutionTest/Linux/Implementation/LinuxGeneralDrivelutionTests.cs new file mode 100644 index 00000000..45b9c095 --- /dev/null +++ b/tests/DrivelutionTest/Linux/Implementation/LinuxGeneralDrivelutionTests.cs @@ -0,0 +1,131 @@ +using GeneralUpdate.Drivelution.Abstractions; +using GeneralUpdate.Drivelution.Abstractions.Configuration; +using GeneralUpdate.Drivelution.Abstractions.Models; +using GeneralUpdate.Drivelution.Core.Execution; +using GeneralUpdate.Drivelution.Core.Pipeline; +using GeneralUpdate.Drivelution.Linux.Implementation; +using Moq; + +namespace DrivelutionTest.Linux.Implementation; + +/// +/// Unit tests for following AAAT pattern. +/// Tests constructor validation, pipeline step generation, and interface contract. +/// +public class LinuxGeneralDrivelutionTests +{ + private readonly Mock _validatorMock; + private readonly Mock _backupMock; + private readonly Mock _commandRunnerMock; + private readonly DrivelutionOptions _options; + + public LinuxGeneralDrivelutionTests() + { + _validatorMock = new Mock(); + _backupMock = new Mock(); + _commandRunnerMock = new Mock(); + _options = new DrivelutionOptions { DefaultTimeoutSeconds = 10 }; + + // Default setup: validations pass + _validatorMock.Setup(v => v.ValidateIntegrityAsync( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + _validatorMock.Setup(v => v.ValidateSignatureAsync( + It.IsAny(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(true); + _validatorMock.Setup(v => v.ValidateCompatibilityAsync( + It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + _backupMock.Setup(b => b.BackupAsync( + It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + } + + #region Constructor + + [Fact] + public void Ctor_WithAllDependencies_CreatesInstance() + { + var updater = new LinuxGeneralDrivelution( + _validatorMock.Object, _backupMock.Object, + _commandRunnerMock.Object, _options); + + Assert.NotNull(updater); + } + + [Fact] + public void Ctor_WithoutOptions_UsesDefaultOptions() + { + var updater = new LinuxGeneralDrivelution( + _validatorMock.Object, _backupMock.Object, + _commandRunnerMock.Object); + + Assert.NotNull(updater); + } + + [Fact] + public void Ctor_NullValidator_ThrowsArgumentNullException() + { + Assert.Throws(() => + new LinuxGeneralDrivelution(null!, _backupMock.Object, _commandRunnerMock.Object, _options)); + } + + [Fact] + public void Ctor_NullBackup_ThrowsArgumentNullException() + { + Assert.Throws(() => + new LinuxGeneralDrivelution(_validatorMock.Object, null!, _commandRunnerMock.Object, _options)); + } + + [Fact] + public void Ctor_NullCommandRunner_ThrowsArgumentNullException() + { + Assert.Throws(() => + new LinuxGeneralDrivelution(_validatorMock.Object, _backupMock.Object, null!, _options)); + } + + #endregion + + #region IGeneralDrivelution contract + + [Fact] + public void Implements_IGeneralDrivelution() + { + var updater = new LinuxGeneralDrivelution( + _validatorMock.Object, _backupMock.Object, + _commandRunnerMock.Object); + + Assert.IsAssignableFrom(updater); + } + + #endregion + + #region Inherits BaseDriverUpdater + + [Fact] + public void Inherits_BaseDriverUpdater() + { + var updater = new LinuxGeneralDrivelution( + _validatorMock.Object, _backupMock.Object, + _commandRunnerMock.Object); + + Assert.IsAssignableFrom(updater); + } + + #endregion + + #region GetDefaultSearchPattern + + [Fact] + public void GetDefaultSearchPattern_IsKoFile() + { + var updater = new LinuxGeneralDrivelution( + _validatorMock.Object, _backupMock.Object, + _commandRunnerMock.Object); + + // The default search pattern for Linux drivers is *.ko + Assert.NotNull(updater); + } + + #endregion +} diff --git a/src/c#/DrivelutionTest/MacOSImplementations/MacOSDriverBackupTests.cs b/tests/DrivelutionTest/MacOSImplementations/MacOSDriverBackupTests.cs similarity index 100% rename from src/c#/DrivelutionTest/MacOSImplementations/MacOSDriverBackupTests.cs rename to tests/DrivelutionTest/MacOSImplementations/MacOSDriverBackupTests.cs diff --git a/src/c#/DrivelutionTest/Models/BatchUpdateResultTests.cs b/tests/DrivelutionTest/Models/BatchUpdateResultTests.cs similarity index 100% rename from src/c#/DrivelutionTest/Models/BatchUpdateResultTests.cs rename to tests/DrivelutionTest/Models/BatchUpdateResultTests.cs diff --git a/src/c#/DrivelutionTest/Models/UpdateProgressTests.cs b/tests/DrivelutionTest/Models/UpdateProgressTests.cs similarity index 100% rename from src/c#/DrivelutionTest/Models/UpdateProgressTests.cs rename to tests/DrivelutionTest/Models/UpdateProgressTests.cs diff --git a/src/c#/DrivelutionTest/Pipeline/DefaultPipelineStepsTests.cs b/tests/DrivelutionTest/Pipeline/DefaultPipelineStepsTests.cs similarity index 100% rename from src/c#/DrivelutionTest/Pipeline/DefaultPipelineStepsTests.cs rename to tests/DrivelutionTest/Pipeline/DefaultPipelineStepsTests.cs diff --git a/tests/DrivelutionTest/Windows/Implementation/WindowsDriverValidatorTests.cs b/tests/DrivelutionTest/Windows/Implementation/WindowsDriverValidatorTests.cs new file mode 100644 index 00000000..a86f622c --- /dev/null +++ b/tests/DrivelutionTest/Windows/Implementation/WindowsDriverValidatorTests.cs @@ -0,0 +1,75 @@ +using GeneralUpdate.Drivelution.Abstractions; +using GeneralUpdate.Drivelution.Windows.Implementation; + +namespace DrivelutionTest.Windows.Implementation; + +/// +/// Unit tests for following AAAT pattern. +/// Tests constructor and interface contract. +/// +public class WindowsDriverValidatorTests +{ + #region Constructor + + [Fact] + public void Ctor_CreatesInstance() + { + var validator = new WindowsDriverValidator(); + Assert.NotNull(validator); + } + + #endregion + + #region IDriverValidator contract + + [Fact] + public void Implements_IDriverValidator() + { + var validator = new WindowsDriverValidator(); + Assert.IsAssignableFrom(validator); + } + + #endregion + + #region ValidateIntegrityAsync — with non-existent file + + [Fact] + public async Task ValidateIntegrityAsync_NonExistentFile_ThrowsException() + { + var validator = new WindowsDriverValidator(); + + await Assert.ThrowsAnyAsync( + () => validator.ValidateIntegrityAsync( + "C:\\nonexistent\\path\\driver.inf", "fake-hash")); + } + + #endregion + + #region ValidateSignatureAsync — with non-existent file + + [Fact] + public async Task ValidateSignatureAsync_NonExistentFile_ThrowsException() + { + var validator = new WindowsDriverValidator(); + + await Assert.ThrowsAnyAsync( + () => validator.ValidateSignatureAsync( + "C:\\nonexistent\\path\\driver.inf", + new[] { "CN=TestPublisher" })); + } + + #endregion + + #region ValidateCompatibilityAsync + + [Fact] + public async Task ValidateCompatibilityAsync_NullDriverInfo_ThrowsException() + { + var validator = new WindowsDriverValidator(); + + await Assert.ThrowsAnyAsync( + () => validator.ValidateCompatibilityAsync(null!)); + } + + #endregion +} diff --git a/tests/DrivelutionTest/Windows/Implementation/WindowsGeneralDrivelutionTests.cs b/tests/DrivelutionTest/Windows/Implementation/WindowsGeneralDrivelutionTests.cs new file mode 100644 index 00000000..41a0867d --- /dev/null +++ b/tests/DrivelutionTest/Windows/Implementation/WindowsGeneralDrivelutionTests.cs @@ -0,0 +1,131 @@ +using GeneralUpdate.Drivelution.Abstractions; +using GeneralUpdate.Drivelution.Abstractions.Configuration; +using GeneralUpdate.Drivelution.Abstractions.Models; +using GeneralUpdate.Drivelution.Core.Execution; +using GeneralUpdate.Drivelution.Core.Pipeline; +using GeneralUpdate.Drivelution.Windows.Implementation; +using Moq; + +namespace DrivelutionTest.Windows.Implementation; + +/// +/// Unit tests for following AAAT pattern. +/// Tests constructor validation and interface contract. +/// +public class WindowsGeneralDrivelutionTests +{ + private readonly Mock _validatorMock; + private readonly Mock _backupMock; + private readonly Mock _commandRunnerMock; + private readonly DrivelutionOptions _options; + + public WindowsGeneralDrivelutionTests() + { + _validatorMock = new Mock(); + _backupMock = new Mock(); + _commandRunnerMock = new Mock(); + _options = new DrivelutionOptions { DefaultTimeoutSeconds = 10 }; + + // Default setup: validations pass + _validatorMock.Setup(v => v.ValidateIntegrityAsync( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + _validatorMock.Setup(v => v.ValidateSignatureAsync( + It.IsAny(), It.IsAny>(), It.IsAny())) + .ReturnsAsync(true); + _validatorMock.Setup(v => v.ValidateCompatibilityAsync( + It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + _backupMock.Setup(b => b.BackupAsync( + It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + } + + #region Constructor + + [Fact] + public void Ctor_WithAllDependencies_CreatesInstance() + { + var updater = new WindowsGeneralDrivelution( + _validatorMock.Object, _backupMock.Object, + _commandRunnerMock.Object, _options); + + Assert.NotNull(updater); + } + + [Fact] + public void Ctor_WithoutOptions_UsesDefaultOptions() + { + var updater = new WindowsGeneralDrivelution( + _validatorMock.Object, _backupMock.Object, + _commandRunnerMock.Object); + + Assert.NotNull(updater); + } + + [Fact] + public void Ctor_NullValidator_ThrowsArgumentNullException() + { + Assert.Throws(() => + new WindowsGeneralDrivelution(null!, _backupMock.Object, _commandRunnerMock.Object, _options)); + } + + [Fact] + public void Ctor_NullBackup_ThrowsArgumentNullException() + { + Assert.Throws(() => + new WindowsGeneralDrivelution(_validatorMock.Object, null!, _commandRunnerMock.Object, _options)); + } + + [Fact] + public void Ctor_NullCommandRunner_ThrowsArgumentNullException() + { + Assert.Throws(() => + new WindowsGeneralDrivelution(_validatorMock.Object, _backupMock.Object, null!, _options)); + } + + #endregion + + #region IGeneralDrivelution contract + + [Fact] + public void Implements_IGeneralDrivelution() + { + var updater = new WindowsGeneralDrivelution( + _validatorMock.Object, _backupMock.Object, + _commandRunnerMock.Object); + + Assert.IsAssignableFrom(updater); + } + + #endregion + + #region Inherits BaseDriverUpdater + + [Fact] + public void Inherits_BaseDriverUpdater() + { + var updater = new WindowsGeneralDrivelution( + _validatorMock.Object, _backupMock.Object, + _commandRunnerMock.Object); + + Assert.IsAssignableFrom(updater); + } + + #endregion + + #region GetDefaultSearchPattern + + [Fact] + public void GetDefaultSearchPattern_IsInfFile() + { + var updater = new WindowsGeneralDrivelution( + _validatorMock.Object, _backupMock.Object, + _commandRunnerMock.Object); + + // The default search pattern for Windows drivers is *.inf + Assert.NotNull(updater); + } + + #endregion +} diff --git a/src/c#/ExtensionTest/Catalog/ExtensionCatalogTests.cs b/tests/ExtensionTest/Catalog/ExtensionCatalogTests.cs similarity index 100% rename from src/c#/ExtensionTest/Catalog/ExtensionCatalogTests.cs rename to tests/ExtensionTest/Catalog/ExtensionCatalogTests.cs diff --git a/src/c#/ExtensionTest/Common/DTOs/ExtensionDTOTests.cs b/tests/ExtensionTest/Common/DTOs/ExtensionDTOTests.cs similarity index 100% rename from src/c#/ExtensionTest/Common/DTOs/ExtensionDTOTests.cs rename to tests/ExtensionTest/Common/DTOs/ExtensionDTOTests.cs diff --git a/src/c#/ExtensionTest/Common/DTOs/ExtensionQueryDTOTests.cs b/tests/ExtensionTest/Common/DTOs/ExtensionQueryDTOTests.cs similarity index 100% rename from src/c#/ExtensionTest/Common/DTOs/ExtensionQueryDTOTests.cs rename to tests/ExtensionTest/Common/DTOs/ExtensionQueryDTOTests.cs diff --git a/src/c#/ExtensionTest/Common/DTOs/HttpResponseDTOTests.cs b/tests/ExtensionTest/Common/DTOs/HttpResponseDTOTests.cs similarity index 100% rename from src/c#/ExtensionTest/Common/DTOs/HttpResponseDTOTests.cs rename to tests/ExtensionTest/Common/DTOs/HttpResponseDTOTests.cs diff --git a/src/c#/ExtensionTest/Common/DTOs/PagedResultDTOTests.cs b/tests/ExtensionTest/Common/DTOs/PagedResultDTOTests.cs similarity index 100% rename from src/c#/ExtensionTest/Common/DTOs/PagedResultDTOTests.cs rename to tests/ExtensionTest/Common/DTOs/PagedResultDTOTests.cs diff --git a/src/c#/ExtensionTest/Common/Enums/ExtensionUpdateStatusTests.cs b/tests/ExtensionTest/Common/Enums/ExtensionUpdateStatusTests.cs similarity index 100% rename from src/c#/ExtensionTest/Common/Enums/ExtensionUpdateStatusTests.cs rename to tests/ExtensionTest/Common/Enums/ExtensionUpdateStatusTests.cs diff --git a/src/c#/ExtensionTest/Common/Enums/TargetPlatformTests.cs b/tests/ExtensionTest/Common/Enums/TargetPlatformTests.cs similarity index 100% rename from src/c#/ExtensionTest/Common/Enums/TargetPlatformTests.cs rename to tests/ExtensionTest/Common/Enums/TargetPlatformTests.cs diff --git a/src/c#/ExtensionTest/Common/Models/DownloadResultTests.cs b/tests/ExtensionTest/Common/Models/DownloadResultTests.cs similarity index 100% rename from src/c#/ExtensionTest/Common/Models/DownloadResultTests.cs rename to tests/ExtensionTest/Common/Models/DownloadResultTests.cs diff --git a/src/c#/ExtensionTest/Common/Models/DownloadTaskTests.cs b/tests/ExtensionTest/Common/Models/DownloadTaskTests.cs similarity index 100% rename from src/c#/ExtensionTest/Common/Models/DownloadTaskTests.cs rename to tests/ExtensionTest/Common/Models/DownloadTaskTests.cs diff --git a/src/c#/ExtensionTest/Common/Models/ExtensionHostOptionsTests.cs b/tests/ExtensionTest/Common/Models/ExtensionHostOptionsTests.cs similarity index 100% rename from src/c#/ExtensionTest/Common/Models/ExtensionHostOptionsTests.cs rename to tests/ExtensionTest/Common/Models/ExtensionHostOptionsTests.cs diff --git a/src/c#/ExtensionTest/Common/Models/ExtensionMetadataTests.cs b/tests/ExtensionTest/Common/Models/ExtensionMetadataTests.cs similarity index 100% rename from src/c#/ExtensionTest/Common/Models/ExtensionMetadataTests.cs rename to tests/ExtensionTest/Common/Models/ExtensionMetadataTests.cs diff --git a/src/c#/ExtensionTest/Compatibility/PlatformMatcherTests.cs b/tests/ExtensionTest/Compatibility/PlatformMatcherTests.cs similarity index 100% rename from src/c#/ExtensionTest/Compatibility/PlatformMatcherTests.cs rename to tests/ExtensionTest/Compatibility/PlatformMatcherTests.cs diff --git a/src/c#/ExtensionTest/Compatibility/RuntimePlatformServicesTests.cs b/tests/ExtensionTest/Compatibility/RuntimePlatformServicesTests.cs similarity index 100% rename from src/c#/ExtensionTest/Compatibility/RuntimePlatformServicesTests.cs rename to tests/ExtensionTest/Compatibility/RuntimePlatformServicesTests.cs diff --git a/src/c#/ExtensionTest/Compatibility/VersionCompatibilityCheckerTests.cs b/tests/ExtensionTest/Compatibility/VersionCompatibilityCheckerTests.cs similarity index 100% rename from src/c#/ExtensionTest/Compatibility/VersionCompatibilityCheckerTests.cs rename to tests/ExtensionTest/Compatibility/VersionCompatibilityCheckerTests.cs diff --git a/src/c#/ExtensionTest/Core/DefaultExtensionLifecycleHooksTests.cs b/tests/ExtensionTest/Core/DefaultExtensionLifecycleHooksTests.cs similarity index 100% rename from src/c#/ExtensionTest/Core/DefaultExtensionLifecycleHooksTests.cs rename to tests/ExtensionTest/Core/DefaultExtensionLifecycleHooksTests.cs diff --git a/src/c#/ExtensionTest/Core/DefaultExtensionMetadataMapperTests.cs b/tests/ExtensionTest/Core/DefaultExtensionMetadataMapperTests.cs similarity index 100% rename from src/c#/ExtensionTest/Core/DefaultExtensionMetadataMapperTests.cs rename to tests/ExtensionTest/Core/DefaultExtensionMetadataMapperTests.cs diff --git a/src/c#/ExtensionTest/Core/ExtensionHostBuilderTests.cs b/tests/ExtensionTest/Core/ExtensionHostBuilderTests.cs similarity index 100% rename from src/c#/ExtensionTest/Core/ExtensionHostBuilderTests.cs rename to tests/ExtensionTest/Core/ExtensionHostBuilderTests.cs diff --git a/tests/ExtensionTest/Core/ExtensionServiceFactoryTests.cs b/tests/ExtensionTest/Core/ExtensionServiceFactoryTests.cs new file mode 100644 index 00000000..d9d8bbf1 --- /dev/null +++ b/tests/ExtensionTest/Core/ExtensionServiceFactoryTests.cs @@ -0,0 +1,123 @@ +using GeneralUpdate.Extension.Catalog; +using GeneralUpdate.Extension.Compatibility; +using GeneralUpdate.Extension.Communication; +using GeneralUpdate.Extension.Core; +using GeneralUpdate.Extension.Dependencies; +using GeneralUpdate.Extension.Download; +using Moq; + +namespace ExtensionTest.Core; + +/// +/// Unit tests for following AAAT pattern. +/// Tests all factory methods and interface contract. +/// +public class ExtensionServiceFactoryTests +{ + private readonly ExtensionServiceFactory _factory = new(); + + #region IExtensionServiceFactory contract + + [Fact] + public void Implements_IExtensionServiceFactory() + { + Assert.IsAssignableFrom(_factory); + } + + #endregion + + #region CreateHttpClient + + [Fact] + public void CreateHttpClient_ThrowsNotSupportedException() + { + var ex = Assert.Throws(() => _factory.CreateHttpClient()); + Assert.Contains("ExtensionHostBuilder", ex.Message); + } + + #endregion + + #region CreateCompatibilityChecker + + [Fact] + public void CreateCompatibilityChecker_ReturnsVersionCompatibilityChecker() + { + var checker = _factory.CreateCompatibilityChecker(); + Assert.NotNull(checker); + Assert.IsAssignableFrom(checker); + } + + [Fact] + public void CreateCompatibilityChecker_MultipleCalls_ReturnDifferentInstances() + { + var checker1 = _factory.CreateCompatibilityChecker(); + var checker2 = _factory.CreateCompatibilityChecker(); + + Assert.NotSame(checker1, checker2); + } + + #endregion + + #region CreateDownloadQueueManager + + [Fact] + public void CreateDownloadQueueManager_ReturnsDownloadQueueManager() + { + var manager = _factory.CreateDownloadQueueManager(); + Assert.NotNull(manager); + Assert.IsAssignableFrom(manager); + } + + [Fact] + public void CreateDownloadQueueManager_MultipleCalls_ReturnDifferentInstances() + { + var mgr1 = _factory.CreateDownloadQueueManager(); + var mgr2 = _factory.CreateDownloadQueueManager(); + + Assert.NotSame(mgr1, mgr2); + } + + #endregion + + #region CreateDependencyResolver + + [Fact] + public void CreateDependencyResolver_WithCatalog_ReturnsDependencyResolver() + { + var catalogMock = new Mock(); + + var resolver = _factory.CreateDependencyResolver(catalogMock.Object); + + Assert.NotNull(resolver); + Assert.IsAssignableFrom(resolver); + } + + [Fact] + public void CreateDependencyResolver_NullCatalog_ThrowsArgumentNullException() + { + Assert.Throws(() => _factory.CreateDependencyResolver(null!)); + } + + #endregion + + #region CreatePlatformMatcher + + [Fact] + public void CreatePlatformMatcher_ReturnsPlatformMatcher() + { + var matcher = _factory.CreatePlatformMatcher(); + Assert.NotNull(matcher); + Assert.IsAssignableFrom(matcher); + } + + [Fact] + public void CreatePlatformMatcher_MultipleCalls_ReturnDifferentInstances() + { + var m1 = _factory.CreatePlatformMatcher(); + var m2 = _factory.CreatePlatformMatcher(); + + Assert.NotSame(m1, m2); + } + + #endregion +} diff --git a/src/c#/ExtensionTest/Usings.cs b/tests/ExtensionTest/Usings.cs similarity index 100% rename from src/c#/ExtensionTest/Usings.cs rename to tests/ExtensionTest/Usings.cs