From 78d9e68fea8ae5da76f79678480e7f4495b4ac7b Mon Sep 17 00:00:00 2001 From: zerlei <1445089819@qq.com> Date: Tue, 30 Jul 2024 15:36:42 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=E5=B0=86=E6=AF=8F=E4=B8=AA=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=96=B9=E6=B3=95=E6=94=B9=E4=B8=BA=E7=9B=B8=E4=BA=92?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原因是: 1. 相互独立的单元测试,有助于debug问题, 2. 不需要考虑执行顺序,额外的运行环境等,更加简单。 做的工作是: 1. 去掉了顺序执行,这有助于并发调用,减少测试时间 2. 去掉了测试方法中共享的实例 --- Server/Common/Common.csproj | 4 + Server/Common/Dir.cs | 2 +- Server/Common/FileDirOp.cs | 137 ++++++++++++++++-- .../Controllers/LocalServerController.cs | 33 +++++ .../Controllers/WeatherForecastController.cs | 33 ----- Server/LocalServer/LocalSyncServer.cs | 19 +++ Server/LocalServer/LocalSyncServerFactory.cs | 22 +++ Server/LocalServer/Program.cs | 54 +++---- Server/LocalServer/WeatherForecast.cs | 13 -- Server/ServerTest/DirFileOpTest.cs | 52 ++++--- Server/ServerTest/FilesSeed.cs | 12 +- Server/ServerTest/PriorityOrderer.cs | 41 ------ Server/ServerTest/TestPriorityAttribute.cs | 9 -- 13 files changed, 265 insertions(+), 166 deletions(-) create mode 100644 Server/LocalServer/Controllers/LocalServerController.cs delete mode 100644 Server/LocalServer/Controllers/WeatherForecastController.cs create mode 100644 Server/LocalServer/LocalSyncServer.cs create mode 100644 Server/LocalServer/LocalSyncServerFactory.cs delete mode 100644 Server/LocalServer/WeatherForecast.cs delete mode 100644 Server/ServerTest/PriorityOrderer.cs delete mode 100644 Server/ServerTest/TestPriorityAttribute.cs diff --git a/Server/Common/Common.csproj b/Server/Common/Common.csproj index fa71b7a..ca2ca29 100644 --- a/Server/Common/Common.csproj +++ b/Server/Common/Common.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/Server/Common/Dir.cs b/Server/Common/Dir.cs index 8c50004..6c47f34 100644 --- a/Server/Common/Dir.cs +++ b/Server/Common/Dir.cs @@ -465,7 +465,7 @@ public class Dir(string path, List? children = null, NextOpType? nex { var ldir = this; var rdir = other; - Dir? cDir = new Dir(rdir.FormatedPath); + Dir? cDir = new(rdir.FormatedPath); //分别对文件和文件夹分组 List lFiles = []; List rFiles = []; diff --git a/Server/Common/FileDirOp.cs b/Server/Common/FileDirOp.cs index 8a0cd6b..977ecaa 100644 --- a/Server/Common/FileDirOp.cs +++ b/Server/Common/FileDirOp.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; +using ICSharpCode.SharpZipLib.Zip; using Microsoft.VisualBasic; namespace Common; @@ -26,7 +27,8 @@ public abstract class FileDirOpStra /// 文件目录打包 /// /// -public class FileDirOpForPack(string srcRootPath, string dstRootPath) : FileDirOpStra +public class FileDirOpForPack(string srcRootPath, string dstRootPath, string syncId = "") + : FileDirOpStra { /// /// 目标根目录 @@ -38,12 +40,78 @@ public class FileDirOpForPack(string srcRootPath, string dstRootPath) : FileDirO /// public readonly string SrcRootPath = srcRootPath; + public readonly string SyncId = string.IsNullOrEmpty(syncId) + ? Guid.NewGuid().ToString() + : syncId; + /// /// 最终完成时的压缩 /// public void FinallyCompress() { - var x = DstRootPath; + static List GetFilesResus(string dirPath) + { + var files = new List(); + foreach (var file in Directory.GetFiles(dirPath)) + { + files.Add(file); + } + foreach (var dir in Directory.GetDirectories(dirPath)) + { + foreach (var file in GetFilesResus(dir)) + { + files.Add(file); + } + } + return files; + } + var fileNames = GetFilesResus(SrcRootPath); + var OuptPutFile = Path.GetDirectoryName(DstRootPath) + $"/{SyncId}.zip"; + using FileStream fsOut = new(OuptPutFile, FileMode.Create); + using ZipOutputStream zipStream = new(fsOut); + { + zipStream.SetLevel(9); // 设置压缩级别 + zipStream.Password = "VSXsdf.123d7802zw@#4_"; // 设置密码 + byte[] buffer = new byte[4096]; + + foreach (string file in fileNames) + { + // Using GetFileName makes the result compatible with XP + // as the resulting path is not absolute. + var entry = new ZipEntry(file.Replace(SrcRootPath, "")); + + // Setup the entry data as required. + + // Crc and size are handled by the library for seakable streams + // so no need to do them here. + + // Could also use the last write time or similar for the file. + //entry.DateTime = ; + entry.DateTime = System.IO.File.GetLastWriteTime(file); + zipStream.PutNextEntry(entry); + + using (FileStream fs = System.IO.File.OpenRead(file)) + { + // Using a fixed size buffer here makes no noticeable difference for output + // but keeps a lid on memory usage. + int sourceBytes; + do + { + sourceBytes = fs.Read(buffer, 0, buffer.Length); + zipStream.Write(buffer, 0, sourceBytes); + } while (sourceBytes > 0); + } + } + + // Finish/Close arent needed strictly as the using statement does this automatically + + // Finish is important to ensure trailing information for a Zip file is appended. Without this + // the created file would be invalid. + zipStream.Finish(); + + // Close is important to wrap things up and unlock the file. + zipStream.Close(); + } } /// @@ -97,27 +165,70 @@ public class FileDirOpForPack(string srcRootPath, string dstRootPath) : FileDirO this.FileCreate(absolutePath, mtime); } - public override void FileDel(string absolutePath) - { - throw new NotImplementedException(); - } + public override void FileDel(string absolutePath) { } - public override void DirDel(Dir dir, bool IsRecursion = true) - { - throw new NotImplementedException(); - } + public override void DirDel(Dir dir, bool IsRecursion = true) { } } -public class FileDirOpForUnpack(string srcCompressedPath, string dstRootPath) : FileDirOpStra +public class FileDirOpForUnpack(string srcRootPath, string dstRootPath, string syncId) + : FileDirOpStra { /// /// 解压缩,必须首先调用 /// public void FirstUnComparess() { - var x = SrcCompressedPath; + string zipFilePath = $"{SrcRootPath}/{SyncId}.zip"; + + using (ZipInputStream s = new ZipInputStream(System.IO.File.OpenRead(zipFilePath))) + { + s.Password = "VSXsdf.123d7802zw@#4_"; + ZipEntry theEntry; + while ((theEntry = s.GetNextEntry()) != null) + { + Console.WriteLine(theEntry.Name); + + string directoryName = + DstRootPath + $"/{SyncId}/" + Path.GetDirectoryName(theEntry.Name) + ?? throw new NullReferenceException("无法得到父文件目录!"); + string fileName = Path.GetFileName(theEntry.Name); + + // create directory + if (directoryName.Length > 0) + { + Directory.CreateDirectory(directoryName); + } + + if (fileName != String.Empty) + { + using ( + FileStream streamWriter = System.IO.File.Create( + directoryName + "/" + fileName + ) + ) + { + int size = 2048; + byte[] data = new byte[2048]; + while (true) + { + size = s.Read(data, 0, data.Length); + if (size > 0) + { + streamWriter.Write(data, 0, size); + } + else + { + break; + } + } + } + } + } + } } + public readonly string SyncId = syncId; + /// /// 目标根目录 /// @@ -126,7 +237,7 @@ public class FileDirOpForUnpack(string srcCompressedPath, string dstRootPath) : /// /// 源目录 /// - public readonly string SrcCompressedPath = srcCompressedPath; + public readonly string SrcRootPath = srcRootPath; /// /// 最终完成时的压缩 diff --git a/Server/LocalServer/Controllers/LocalServerController.cs b/Server/LocalServer/Controllers/LocalServerController.cs new file mode 100644 index 0000000..2f2f7a9 --- /dev/null +++ b/Server/LocalServer/Controllers/LocalServerController.cs @@ -0,0 +1,33 @@ +using System.Text; +using Microsoft.AspNetCore.Mvc; + +namespace LocalServer.Controllers +{ + public class LocalServerController(LocalSyncServerFactory factory) : ControllerBase + { + private readonly LocalSyncServerFactory Factory = factory; + + [Route("/")] + public async Task WebsocketConnection(string Name) + { + if (HttpContext.WebSockets.IsWebSocketRequest) + { + try + { + var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); + Factory.CreateLocalSyncServer(webSocket, Name); + } + catch (Exception e) + { + HttpContext.Response.Body = new MemoryStream(Encoding.UTF8.GetBytes(e.Message)); + HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable; + } + } + else + { + HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + } + } + //TODO 是否在本地记载同步日志? + } +} diff --git a/Server/LocalServer/Controllers/WeatherForecastController.cs b/Server/LocalServer/Controllers/WeatherForecastController.cs deleted file mode 100644 index f1d4199..0000000 --- a/Server/LocalServer/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace LocalServer.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/Server/LocalServer/LocalSyncServer.cs b/Server/LocalServer/LocalSyncServer.cs new file mode 100644 index 0000000..a55b662 --- /dev/null +++ b/Server/LocalServer/LocalSyncServer.cs @@ -0,0 +1,19 @@ +using System.Net.WebSockets; + +namespace LocalServer; + +public class LocalSyncServer +{ + public readonly WebSocket Socket; + public readonly string Name; + + public readonly LocalSyncServerFactory Factory; + + public LocalSyncServer(WebSocket socket, string name, LocalSyncServerFactory factory) + { + Socket = socket; + Name = name; + Factory = factory; + + } +} diff --git a/Server/LocalServer/LocalSyncServerFactory.cs b/Server/LocalServer/LocalSyncServerFactory.cs new file mode 100644 index 0000000..b06c64f --- /dev/null +++ b/Server/LocalServer/LocalSyncServerFactory.cs @@ -0,0 +1,22 @@ +using System.Net.WebSockets; + +namespace LocalServer; + +public class LocalSyncServerFactory +{ + public void CreateLocalSyncServer(WebSocket socket, string name) + { + if (Servers.Select(x => x.Name == name).Any()) + { + throw new Exception("there already is a server with that name is Runing!"); + } + Servers.Add(new LocalSyncServer(socket, name, this)); + } + + private readonly List Servers = []; + + public void RemoveLocalSyncServer(LocalSyncServer server) + { + Servers.Remove(server); + } +} diff --git a/Server/LocalServer/Program.cs b/Server/LocalServer/Program.cs index c620a67..652ebf8 100644 --- a/Server/LocalServer/Program.cs +++ b/Server/LocalServer/Program.cs @@ -1,34 +1,26 @@ +using LocalServer; +var builder = WebApplication.CreateBuilder(args); -namespace LocalServer +// Add services to the container. + +builder.Services.AddControllers(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) { - public class Program - { - public static void Main(string[] args) - { - var builder = WebApplication.CreateBuilder(args); - - // Add services to the container. - - builder.Services.AddControllers(); - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); - - var app = builder.Build(); - - // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(); - } - - app.UseAuthorization(); - - - app.MapControllers(); - - app.Run(); - } - } + app.UseSwagger(); + app.UseSwaggerUI(); } +app.UseWebSockets(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Server/LocalServer/WeatherForecast.cs b/Server/LocalServer/WeatherForecast.cs deleted file mode 100644 index a7b4de4..0000000 --- a/Server/LocalServer/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace LocalServer -{ - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -} diff --git a/Server/ServerTest/DirFileOpTest.cs b/Server/ServerTest/DirFileOpTest.cs index 09c13c0..a401fad 100644 --- a/Server/ServerTest/DirFileOpTest.cs +++ b/Server/ServerTest/DirFileOpTest.cs @@ -1,31 +1,24 @@ using Common; +using Xunit; + /*using Newtonsoft.Json;*/ -using XUnit.Project.Attributes; namespace ServerTest; -/// -/// xUnit将会对每个测试方法创建一个测试上下文,IClassFixture可以用来创建类中共享测试上下文, -/// -/// XUnit 的测试方法不是按照顺序执行,所以注意对象状态 -/// -/// 一般单元测试,每个测试函数应当是独立的,不让它们按照顺序执行,在一般情况下是最好的做法,参考 -/// https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices -/// 目前涉及到一些文件的同步,所以按照顺序执行相对较好,这使用了xUnit的方法使它们按照顺序执行 -/// -/// -[TestCaseOrderer( - ordererTypeName: "XUnit.Project.Orderers.PriorityOrderer", - ordererAssemblyName: "ServerTest" -)] -public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture +public class DirFileOpTest : IDisposable { - private readonly FilesSeed filesSeed = filesSeed; + private readonly FilesSeed filesSeed = new(); + + public void Dispose() + { + //filesSeed.Dispose(); + GC.SuppressFinalize(this); + } /// /// 测试文件目录写入和提取 /// - [Fact, TestPriority(0)] + [Fact] public void FileDirWriteExtract() { filesSeed.NewDir.WriteByThisInfo(filesSeed.fileDirOp); @@ -41,11 +34,10 @@ public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture /// /// 测试文件差异比较 /// - [Fact, TestPriority(1)] + [Fact] public void FileDirDiff() { var cDDir = filesSeed.NewDir.Diff(filesSeed.OldDir); - // Console.WriteLine("################################"); // Console.WriteLine(cDDir.Children.Count); //Assert.True(IsSuccess); @@ -57,9 +49,10 @@ public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture /// /// 测试同步是否成功 /// - [Fact, TestPriority(2)] + [Fact] public void SyncFileDir() { + filesSeed.OldDir.WriteByThisInfo(filesSeed.fileDirOp); filesSeed.OldDir.CombineJustDirFile(filesSeed.fileDirOp, filesSeed.DiffDir); Dir oldSync = new(filesSeed.OldDir.FormatedPath); oldSync.ExtractInfo(); @@ -70,7 +63,7 @@ public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture /// /// 测试文件合并 /// - [Fact, TestPriority(3)] + [Fact] public void DirsCombine() { filesSeed.OldDir.CombineJustObject(filesSeed.DiffDir); @@ -79,4 +72,19 @@ public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture // Console.WriteLine(filesSeed.OldDir.Path); Assert.True(filesSeed.OldDir.IsEqual(filesSeed.NewDir), "合并结果不一致!"); } + + [Fact] + public void Tt() + { + filesSeed.NewDir.WriteByThisInfo(filesSeed.fileDirOp); + var c = new FileDirOpForPack(filesSeed.NewDir.FormatedPath, filesSeed.TestPath + "/"); + c.FinallyCompress(); + + var d = new FileDirOpForUnpack( + filesSeed.TestPath + "/", + filesSeed.TestPath + "/", + c.SyncId + ); + d.FirstUnComparess(); + } } diff --git a/Server/ServerTest/FilesSeed.cs b/Server/ServerTest/FilesSeed.cs index 2721a97..c5b37e9 100644 --- a/Server/ServerTest/FilesSeed.cs +++ b/Server/ServerTest/FilesSeed.cs @@ -176,7 +176,7 @@ public class FilesSeed : IDisposable fileDirOp = new SimpleFileDirOp(); } - private readonly string TestPath = Path.Combine(Directory.GetCurrentDirectory(), "../../.."); + public readonly string TestPath = Path.Combine(Directory.GetCurrentDirectory(), "../../.."); public Dir NewDir; public Dir OldDir; public Dir DiffDir; @@ -184,8 +184,14 @@ public class FilesSeed : IDisposable public void Dispose() { - Directory.Delete($"{TestPath}/OldDir", true); - Directory.Delete($"{TestPath}/NewDir", true); + if (Directory.Exists($"{TestPath}/OldDir")) + { + Directory.Delete($"{TestPath}/OldDir", true); + } + if (Directory.Exists($"{TestPath}/NewDir")) + { + Directory.Delete($"{TestPath}/NewDir", true); + } Console.WriteLine("FilesSeed Dispose"); GC.SuppressFinalize(this); } diff --git a/Server/ServerTest/PriorityOrderer.cs b/Server/ServerTest/PriorityOrderer.cs deleted file mode 100644 index 519cd6d..0000000 --- a/Server/ServerTest/PriorityOrderer.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Xunit.Abstractions; -using Xunit.Sdk; -using XUnit.Project.Attributes; - -namespace XUnit.Project.Orderers; - -public class PriorityOrderer : ITestCaseOrderer -{ - public IEnumerable OrderTestCases( - IEnumerable testCases) where TTestCase : ITestCase - { - string assemblyName = typeof(TestPriorityAttribute).AssemblyQualifiedName!; - var sortedMethods = new SortedDictionary>(); - foreach (TTestCase testCase in testCases) - { - int priority = testCase.TestMethod.Method - .GetCustomAttributes(assemblyName) - .FirstOrDefault() - ?.GetNamedArgument(nameof(TestPriorityAttribute.Priority)) ?? 0; - - GetOrCreate(sortedMethods, priority).Add(testCase); - } - - foreach (TTestCase testCase in - sortedMethods.Keys.SelectMany( - priority => sortedMethods[priority].OrderBy( - testCase => testCase.TestMethod.Method.Name))) - { - Console.WriteLine(testCase); - yield return testCase; - } - } - - private static TValue GetOrCreate( - IDictionary dictionary, TKey key) - where TKey : struct - where TValue : new() => - dictionary.TryGetValue(key, out TValue? result) - ? result - : (dictionary[key] = new TValue()); -} diff --git a/Server/ServerTest/TestPriorityAttribute.cs b/Server/ServerTest/TestPriorityAttribute.cs deleted file mode 100644 index 690076b..0000000 --- a/Server/ServerTest/TestPriorityAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace XUnit.Project.Attributes; - -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -public class TestPriorityAttribute : Attribute -{ - public int Priority { get; private set; } - - public TestPriorityAttribute(int priority) => Priority = priority; -}