From 1809dbebf54aa6f8858179d4c9a5b5dc3f77fa6e Mon Sep 17 00:00:00 2001 From: zerlei <1445089819@qq.com> Date: Fri, 12 Jul 2024 14:15:38 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=AE=8C=E6=88=90=E4=BA=86=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=93=8D=E4=BD=9C=EF=BC=8C=E7=8E=B0=E5=9C=A8=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E5=AF=B9=E6=97=A7=E6=96=87=E4=BB=B6=E5=A4=B9=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E6=9B=B4=E6=96=B0=E7=AD=96=E7=95=A5=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 写逻辑复杂的程序,单元测试必不可少,重构和修改更有信心 2. 单元测试每个测试函数应该是独立的,不相互依赖的,也应该是简单的,否则出现问题不好排查,它叫单元测试 3. 运行测试和debug的时候不要害怕😰😰😰😰😰🙃🙃🙃🙃🙃❤️❤️❤️❤️❤️,它帮你找到了错误,完全没有否定你的意思····· --- Server/Common/Dir.cs | 190 +++++++++++++++++---- Server/Common/FileDirOp.cs | 90 ++++++++++ Server/ServerTest/DirFileOpTest.cs | 91 +++++----- Server/ServerTest/FilesSeed.cs | 34 ++-- Server/ServerTest/PriorityOrderer.cs | 41 +++++ Server/ServerTest/TestPriorityAttribute.cs | 9 + 6 files changed, 364 insertions(+), 91 deletions(-) create mode 100644 Server/Common/FileDirOp.cs create mode 100644 Server/ServerTest/PriorityOrderer.cs create mode 100644 Server/ServerTest/TestPriorityAttribute.cs diff --git a/Server/Common/Dir.cs b/Server/Common/Dir.cs index 1d17719..f88ec31 100644 --- a/Server/Common/Dir.cs +++ b/Server/Common/Dir.cs @@ -119,11 +119,131 @@ public class Dir(string path, List? children = null, NextOpType? nex } /// - /// 合并两个文件夹,other不会发生改变,this将合并一个副本 + /// 文件夹合并 /// - /// 它的一个clone将被合并的dir,它的NextOp 不应该是空,否则什么都不会发生 + /// 具体操作步骤 + /// 将要更新的内容 + /// 是否更新Object对象 + /// 是否更新文件目录树 /// - public (bool, string) Combine(Dir other) + public (bool, string) Combine( + FileDirOp? fileDirOp, + Dir diffdir, + bool IsUpdateObject = true, + bool IsUpdateDirFile = false + ) + { + if (this.FormatedPath != diffdir.FormatedPath) + { + return (false, "their path is not same"); + } + else + { + var ldir = this; + var rdir = diffdir; + + foreach (var oc in diffdir.Children) + { + if (oc is File rfile) + { + if (rfile.NextOp != null) + { + if (oc.NextOp == NextOpType.Add) + { + if (IsUpdateObject) + { + ldir.AddChild(new File(rfile.FormatedPath, rfile.MTime)); + } + if (IsUpdateDirFile) + { + fileDirOp?.FileCreate(rfile.FormatedPath, rfile.MTime); + } + } + else + { + var n = ldir + .Children.Where(x => + x.FormatedPath == oc.FormatedPath && x.Type == DirOrFile.File + ) + .FirstOrDefault(); + if (n is not null) + { + if (oc.NextOp == NextOpType.Del) + { + if (IsUpdateObject) + { + ldir.Children.Remove(n); + } + if (IsUpdateDirFile) + { + fileDirOp?.FileDel(rfile.FormatedPath); + } + } + else if (oc.NextOp == NextOpType.Modify) + { + if (n is File lfile) + { + if (IsUpdateObject) + { + lfile.MTime = rfile.MTime; + } + if (IsUpdateDirFile) + { + fileDirOp?.FileModify(rfile.FormatedPath, rfile.MTime); + } + } + } + } + } + } + } + else if (oc is Dir rrdir) + { + //新增和删除意味着整个文件夹都被新增和删除 + if (rrdir.NextOp == NextOpType.Add) + { + if (IsUpdateDirFile) + { + fileDirOp?.DirCreate(rrdir); + } + if (IsUpdateObject) + { + ldir.AddChild(rrdir.Clone(null, true)); + } + } + else if (rrdir.NextOp == NextOpType.Del) + { + if (IsUpdateDirFile) + { + fileDirOp?.DirDel(rrdir,false); + } + if (IsUpdateObject) + { + ldir.Children.RemoveAt( + ldir.Children.FindIndex(x => x.FormatedPath == rrdir.FormatedPath) + ); + } + } + //当子文件夹和文件不确定时 + else + { + var n = ldir + .Children.Where(x => + x.FormatedPath == rrdir.FormatedPath && x.Type == DirOrFile.Dir + ) + .FirstOrDefault(); + if (n is Dir lldir) + { + lldir.Combine(fileDirOp, rrdir, IsUpdateObject, IsUpdateDirFile); + } + } + } + } + } + return (true, ""); + } + + public (bool, string) Combine_Old(Dir other) { if (this.FormatedPath != other.FormatedPath) { @@ -191,7 +311,7 @@ public class Dir(string path, List? children = null, NextOpType? nex .FirstOrDefault(); if (n is Dir lldir) { - lldir.Combine(rrdir); + lldir.Combine_Old(rrdir); } } } @@ -200,6 +320,26 @@ public class Dir(string path, List? children = null, NextOpType? nex return (true, ""); } + /// + /// 合并两个文件夹,other不会发生改变,this将合并一个副本,这不会改变文件结构 + /// + /// 它的一个clone将被合并的dir,它的NextOp 不应该是空,否则什么都不会发生 + /// + public (bool, string) CombineJustObject(Dir other) + { + return Combine(null, other, true, false); + } + + /// + /// 合并两个文件夹,other不会发生改变,this将不会改变,而文件结构会改变 + /// + /// 它的一个clone将被合并的dir,它的NextOp 不应该是空,否则什么都不会发生 + /// + public (bool, string) CombineJustDirFile(FileDirOp fileDirOp, Dir diffDir) + { + return Combine(fileDirOp, diffDir, false, true); + } + /// /// 添加子节点,根目录相同,才会被添加进去 /// @@ -207,7 +347,7 @@ public class Dir(string path, List? children = null, NextOpType? nex /// / protected (bool, string) AddChild(AFileOrDir child) { - if (child.FormatedPath.Substring(0, this.FormatedPath.Length) != this.FormatedPath) + if (child.FormatedPath[..this.FormatedPath.Length] != this.FormatedPath) { return (false, "their rootpath are not same!"); } @@ -270,29 +410,32 @@ public class Dir(string path, List? children = null, NextOpType? nex /// 文件的修改时间,是否修改文件的修改时间,需要定义文件的写入策略 WriteFileStrageFunc /// /// - public (bool, string) WriteByThisInfo() + public (bool, string) WriteByThisInfo(FileDirOp fileDirOp) { - static (bool, string) f(Dir dir) + static (bool, string) f(Dir dir, FileDirOp fileDirOp) { foreach (var child in dir.Children) { if (child.Type == DirOrFile.Dir) { - var (IsSuccess, Message) = WriteDirStrageFunc(child.FormatedPath); - if (!IsSuccess) - { - return (false, Message); - } if (child is Dir childDir) { - f(childDir); + var (IsSuccess, Message) = fileDirOp.DirCreate(childDir, false); + if (!IsSuccess) + { + return (false, Message); + } + f(childDir, fileDirOp); } } else { if (child is File childFile) { - var (IsSuccess, Message) = WriteFileStrageFunc(childFile); + var (IsSuccess, Message) = fileDirOp.FileCreate( + child.FormatedPath, + childFile.MTime + ); if (!IsSuccess) { return (false, Message); @@ -306,24 +449,9 @@ public class Dir(string path, List? children = null, NextOpType? nex } return (true, ""); } - return f(this); + return f(this, fileDirOp); } -#pragma warning disable CA2211 // Non-constant fields should not be visible - public static Func WriteFileStrageFunc = (File file) => - { - return (false, "you must implement this function!"); - }; -#pragma warning restore CA2211 // Non-constant fields should not be visible -#pragma warning disable CA2211 // Non-constant fields should not be visible - public static Func WriteDirStrageFunc = (string path) => - { - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - return (true, ""); - }; -#pragma warning restore CA2211 // Non-constant fields should not be visible + /// /// 比较两个目录文件树是否相同,不相同返回差异部分,左侧是右侧的下一个版本 /// diff --git a/Server/Common/FileDirOp.cs b/Server/Common/FileDirOp.cs new file mode 100644 index 0000000..584aea9 --- /dev/null +++ b/Server/Common/FileDirOp.cs @@ -0,0 +1,90 @@ +using System.Text; + +namespace Common; + +public abstract class FileDirOp +{ + public abstract (bool, string) FileCreate(string absolutePath, DateTime mtime); + + public abstract (bool, string) DirCreate(Dir dir, bool IsRecursion = true); + + public abstract (bool, string) FileModify(string absolutePath, DateTime mtime); + public abstract (bool, string) FileDel(string absolutePath); + public abstract (bool, string) DirDel(Dir dir, bool IsRecursion = true); +} + +public class SimpleFileDirOpForTest : FileDirOp +{ + public override (bool, string) FileCreate(string absolutePath, DateTime mtime) + { + using (FileStream fs = System.IO.File.OpenWrite(absolutePath)) + { + byte[] info = Encoding.UTF8.GetBytes($"this is {absolutePath},Now{mtime}"); + fs.Write(info, 0, info.Length); + } + System.IO.File.SetLastWriteTime(absolutePath, mtime); + return (true, ""); + } + + public override (bool, string) FileModify(string absolutePath, DateTime mtime) + { + using (FileStream fs = System.IO.File.OpenWrite(absolutePath)) + { + byte[] info = Encoding.UTF8.GetBytes($"this is {absolutePath},Now{mtime}"); + fs.Write(info, 0, info.Length); + } + System.IO.File.SetLastWriteTime(absolutePath, mtime); + return (true, ""); + } + + public override (bool, string) FileDel(string absolutePath) + { + //ToDo 权限检查 + if (System.IO.File.Exists(absolutePath)) + { + System.IO.File.Delete(absolutePath); + } + return (true, ""); + } + + public override (bool, string) DirCreate(Dir dir, bool IsRecursion = true) + { + //TODO需做权限检查 + if (!Directory.Exists(dir.FormatedPath)) + { + Directory.CreateDirectory(dir.FormatedPath); + if (IsRecursion) + { + foreach (var fd in dir.Children) + { + if (fd is File file) + { + this.FileCreate(file.FormatedPath, file.MTime); + } + else if (fd is Dir sdir) + { + DirCreate(sdir); + } + } + } + } + return (true, ""); + } + + public override (bool, string) DirDel(Dir dir, bool IsRecursion = true) + { + //TODO 权限检查 正式徐执行递归 + if (!IsRecursion) + { + if (Directory.Exists(dir.FormatedPath)) + { + Directory.Delete(dir.FormatedPath, true); + } + return (true, ""); + } + else + { + throw new NotImplementedException(); + } + } +} diff --git a/Server/ServerTest/DirFileOpTest.cs b/Server/ServerTest/DirFileOpTest.cs index d199598..6e691af 100644 --- a/Server/ServerTest/DirFileOpTest.cs +++ b/Server/ServerTest/DirFileOpTest.cs @@ -1,71 +1,86 @@ -namespace ServerTest; using Common; -using Newtonsoft.Json; -using System.Text.Json.Nodes; -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 { private readonly FilesSeed filesSeed = filesSeed; - /// /// 测试文件目录写入和提取 /// - [Fact] + [Fact, TestPriority(0)] public void FileDirWriteExtract() { - var (IsSuccess, Message) = filesSeed.NewDir.WriteByThisInfo(); - Assert.True(IsSuccess); - Dir nd = new(filesSeed.NewDir.FormatedPath); - nd.ExtractInfo(); - Assert.True(nd.IsEqual(filesSeed.NewDir)); + var (IsSuccess, Message) = filesSeed.NewDir.WriteByThisInfo(filesSeed.fileDirOp); + var (IsSuccess2, Message2) = filesSeed.OldDir.WriteByThisInfo(filesSeed.fileDirOp); + Assert.True(IsSuccess, "新文件写入失败!"); + Assert.True(IsSuccess2, "旧文件写入失败!"); + Dir nnd = new(filesSeed.NewDir.FormatedPath); + nnd.ExtractInfo(); + Assert.True(nnd.IsEqual(filesSeed.NewDir), "新文件提取文件夹的信息与写入信息不一致!"); + Dir nod = new(filesSeed.OldDir.FormatedPath); + nod.ExtractInfo(); + Assert.True(nod.IsEqual(filesSeed.OldDir), "旧提取文件夹的信息与写入信息不一致!"); } /// /// 测试文件差异比较 /// - [Fact] + [Fact, TestPriority(1)] public void FileDirDiff() { var cDDir = filesSeed.NewDir.Diff(filesSeed.OldDir); - + // Console.WriteLine("################################"); // Console.WriteLine(cDDir.Children.Count); //Assert.True(IsSuccess); - var str = JsonConvert.SerializeObject(cDDir); - Assert.True(cDDir.Children.Count !=0); - Assert.True(filesSeed.DiffDir.IsEqual(cDDir)); - - } - - /// - /// 测试文件合并 - /// - [Fact] - public void DirsCombine() - { - var OldDirClone = filesSeed.OldDir.Clone(); - var (IsSuccess, Message) = OldDirClone.Combine(filesSeed.DiffDir); - Assert.True(IsSuccess); - //Assert.False(filesSeed.NewDir.IsEqual(filesSeed.OldDir)); - OldDirClone.ResetRootPath("OldDir","NewDir"); - // Console.WriteLine(filesSeed.OldDir.Path); - Assert.True(OldDirClone.IsEqual(filesSeed.NewDir)); - + /*var str = JsonConvert.SerializeObject(cDDir);*/ + Assert.True(filesSeed.DiffDir.IsEqual(cDDir), "文件对比结果错误!"); } /// /// 测试同步是否成功 /// - [Fact] - public void FinalSyncFileDir() + [Fact, TestPriority(2)] + public void SyncFileDir() { - Assert.True(true); - } + var (IsSuccess,Messsage) = filesSeed.OldDir.CombineJustDirFile(filesSeed.fileDirOp,filesSeed.DiffDir); + Assert.True(IsSuccess, "文件更新错误!"); + Dir oldSync = new(filesSeed.OldDir.FormatedPath); + oldSync.ExtractInfo(); + oldSync.ResetRootPath(filesSeed.OldDir.FormatedPath, filesSeed.NewDir.FormatedPath); + Assert.True(oldSync.IsEqual(filesSeed.NewDir), "文件夹同步后信息保持不一致!"); + } + + /// + /// 测试文件合并 + /// + [Fact, TestPriority(3)] + public void DirsCombine() + { + var (IsSuccess, Message) = filesSeed.OldDir.CombineJustObject(filesSeed.DiffDir); + Assert.True(IsSuccess, "文件合并出错!"); + //Assert.False(filesSeed.NewDir.IsEqual(filesSeed.OldDir)); + filesSeed.OldDir.ResetRootPath("OldDir", "NewDir"); + // Console.WriteLine(filesSeed.OldDir.Path); + Assert.True(filesSeed.OldDir.IsEqual(filesSeed.NewDir), "合并结果不一致!"); + } } diff --git a/Server/ServerTest/FilesSeed.cs b/Server/ServerTest/FilesSeed.cs index 9d0afd9..15ccda1 100644 --- a/Server/ServerTest/FilesSeed.cs +++ b/Server/ServerTest/FilesSeed.cs @@ -7,18 +7,6 @@ public class FilesSeed : IDisposable { public FilesSeed() { - Dir.WriteFileStrageFunc = (Common.File file) => - { - //创建或者不创建直接打开文件 - using (FileStream fs = System.IO.File.OpenWrite(file.FormatedPath)) - { - byte[] info = Encoding.UTF8.GetBytes($"this is {file.FormatedPath},Now{DateTime.Now}"); - fs.Write(info, 0, info.Length); - } - Console.WriteLine($"WriteFileStrageFunc {file.FormatedPath}"); - System.IO.File.SetLastWriteTime(file.FormatedPath, file.MTime); - return (true, ""); - }; Console.WriteLine("FilesSeed Construct"); // string TestPath = Path.Combine(Directory.GetCurrentDirectory(), "../../.."); DateTime NewTime = DateTime.Now.AddSeconds(-99); @@ -88,20 +76,20 @@ public class FilesSeed : IDisposable $"{TestPath}/OldDir/2/2_2_M", [ new Common.File( - $"{TestPath}/OldDir/2/2_2/1.txt", + $"{TestPath}/OldDir/2/2_2_M/1.txt", OldTime, NextOpType.Del ), new Common.File( - $"{TestPath}/OldDir/2/2_2/2.txt", + $"{TestPath}/OldDir/2/2_2_M/2.txt", OldTime, NextOpType.Del ), new Dir( - $"{TestPath}/OldDir/2/2_2/2_3", + $"{TestPath}/OldDir/2/2_2_M/2_3", [ new Common.File( - $"{TestPath}/OldDir/2/2_2/2_3/1.txt", + $"{TestPath}/OldDir/2/2_2_M/2_3/1.txt", OldTime, NextOpType.Del ), @@ -168,13 +156,13 @@ public class FilesSeed : IDisposable new Dir( $"{TestPath}/OldDir/2/2_2_M", [ - new Common.File($"{TestPath}/OldDir/2/2_2/1.txt", OldTime), - new Common.File($"{TestPath}/OldDir/2/2_2/2.txt", OldTime), + new Common.File($"{TestPath}/OldDir/2/2_2_M/1.txt", OldTime), + new Common.File($"{TestPath}/OldDir/2/2_2_M/2.txt", OldTime), new Dir( - $"{TestPath}/OldDir/2/2_2/2_3", + $"{TestPath}/OldDir/2/2_2_M/2_3", [ new Common.File( - $"{TestPath}/OldDir/2/2_2/2_3/1.txt", + $"{TestPath}/OldDir/2/2_2_M/2_3/1.txt", OldTime ), ] @@ -185,17 +173,19 @@ public class FilesSeed : IDisposable ), ] ); + fileDirOp = new SimpleFileDirOpForTest(); } private readonly string TestPath = Path.Combine(Directory.GetCurrentDirectory(), "../../.."); public Dir NewDir; public Dir OldDir; - public Dir DiffDir; + public FileDirOp fileDirOp; public void Dispose() { - Directory.Delete(NewDir.FormatedPath, true); + Directory.Delete($"{TestPath}/OldDir", true); + Directory.Delete($"{TestPath}/NewDir", true); Console.WriteLine("FilesSeed Dispose"); GC.SuppressFinalize(this); } diff --git a/Server/ServerTest/PriorityOrderer.cs b/Server/ServerTest/PriorityOrderer.cs new file mode 100644 index 0000000..519cd6d --- /dev/null +++ b/Server/ServerTest/PriorityOrderer.cs @@ -0,0 +1,41 @@ +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 new file mode 100644 index 0000000..690076b --- /dev/null +++ b/Server/ServerTest/TestPriorityAttribute.cs @@ -0,0 +1,9 @@ +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; +}