chore: 完成了文件操作,现在可以对旧文件夹执行更新策略,并添加了单元测试

1. 写逻辑复杂的程序,单元测试必不可少,重构和修改更有信心
2. 单元测试每个测试函数应该是独立的,不相互依赖的,也应该是简单的,否则出现问题不好排查,它叫单元测试
3. 运行测试和debug的时候不要害怕😰😰😰😰😰🙃🙃🙃🙃🙃❤️❤️❤️❤️❤️,它帮你找到了错误,完全没有否定你的意思·····
This commit is contained in:
zerlei 2024-07-12 14:15:38 +08:00
parent 46315ba760
commit 1809dbebf5
6 changed files with 364 additions and 91 deletions

View file

@ -119,11 +119,131 @@ public class Dir(string path, List<AFileOrDir>? children = null, NextOpType? nex
}
/// <summary>
/// 合并两个文件夹,other不会发生改变this将合并一个副本
/// 文件夹合并
/// </summary>
/// <param name="other">它的一个clone将被合并的dir,它的NextOp 不应该是空,否则什么都不会发生</param>
/// <param name="fileDirOp">具体操作步骤</param>
/// <param name="diffdir">将要更新的内容</param>
/// <param name="IsUpdateObject"> 是否更新Object对象</param>
/// <param name="IsUpdateDirFile">是否更新文件目录树</param>
/// <returns></returns>
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<AFileOrDir>? 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<AFileOrDir>? children = null, NextOpType? nex
return (true, "");
}
/// <summary>
/// 合并两个文件夹,other不会发生改变this将合并一个副本这不会改变文件结构
/// </summary>
/// <param name="other">它的一个clone将被合并的dir,它的NextOp 不应该是空,否则什么都不会发生</param>
/// <returns></returns>
public (bool, string) CombineJustObject(Dir other)
{
return Combine(null, other, true, false);
}
/// <summary>
/// 合并两个文件夹,other不会发生改变this将不会改变而文件结构会改变
/// </summary>
/// <param name="other">它的一个clone将被合并的dir,它的NextOp 不应该是空,否则什么都不会发生</param>
/// <returns></returns>
public (bool, string) CombineJustDirFile(FileDirOp fileDirOp, Dir diffDir)
{
return Combine(fileDirOp, diffDir, false, true);
}
/// <summary>
/// 添加子节点,根目录相同,才会被添加进去
/// </summary>
@ -207,7 +347,7 @@ public class Dir(string path, List<AFileOrDir>? children = null, NextOpType? nex
/// <returns></returns>/
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<AFileOrDir>? children = null, NextOpType? nex
/// 文件的修改时间,是否修改文件的修改时间,需要定义文件的写入策略 WriteFileStrageFunc
/// </summary>
/// <returns></returns>
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 (child is Dir childDir)
{
var (IsSuccess, Message) = fileDirOp.DirCreate(childDir, false);
if (!IsSuccess)
{
return (false, Message);
}
if (child is Dir childDir)
{
f(childDir);
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<AFileOrDir>? 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<File, (bool, string)> 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<string, (bool, string)> WriteDirStrageFunc = (string path) =>
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
return (true, "");
};
#pragma warning restore CA2211 // Non-constant fields should not be visible
/// <summary>
/// 比较两个目录文件树是否相同,不相同返回差异部分,左侧是右侧的下一个版本
/// </summary>

View file

@ -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();
}
}
}

View file

@ -1,35 +1,49 @@
namespace ServerTest;
using Common;
using Newtonsoft.Json;
using System.Text.Json.Nodes;
using Xunit;
/*using Newtonsoft.Json;*/
using XUnit.Project.Attributes;
namespace ServerTest;
/// <summary>
/// xUnit将会对每个测试方法创建一个测试上下文IClassFixture可以用来创建类中共享测试上下文
///
/// XUnit 的测试方法不是按照顺序执行,所以注意对象状态
///
/// 一般单元测试,每个测试函数应当是独立的,不让它们按照顺序执行,在一般情况下是最好的做法,参考
/// https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices
/// 目前涉及到一些文件的同步所以按照顺序执行相对较好这使用了xUnit的方法使它们按照顺序执行
/// </summary>
///
[TestCaseOrderer(
ordererTypeName: "XUnit.Project.Orderers.PriorityOrderer",
ordererAssemblyName: "ServerTest"
)]
public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture<FilesSeed>
{
private readonly FilesSeed filesSeed = filesSeed;
/// <summary>
/// 测试文件目录写入和提取
/// </summary>
[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), "旧提取文件夹的信息与写入信息不一致!");
}
/// <summary>
/// 测试文件差异比较
/// </summary>
[Fact]
[Fact, TestPriority(1)]
public void FileDirDiff()
{
var cDDir = filesSeed.NewDir.Diff(filesSeed.OldDir);
@ -38,34 +52,35 @@ public class DirFileOpTest(FilesSeed filesSeed) : IClassFixture<FilesSeed>
// 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));
}
/// <summary>
/// 测试文件合并
/// </summary>
[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), "文件对比结果错误!");
}
/// <summary>
/// 测试同步是否成功
/// </summary>
[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), "文件夹同步后信息保持不一致!");
}
/// <summary>
/// 测试文件合并
/// </summary>
[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), "合并结果不一致!");
}
}

View file

@ -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);
}

View file

@ -0,0 +1,41 @@
using Xunit.Abstractions;
using Xunit.Sdk;
using XUnit.Project.Attributes;
namespace XUnit.Project.Orderers;
public class PriorityOrderer : ITestCaseOrderer
{
public IEnumerable<TTestCase> OrderTestCases<TTestCase>(
IEnumerable<TTestCase> testCases) where TTestCase : ITestCase
{
string assemblyName = typeof(TestPriorityAttribute).AssemblyQualifiedName!;
var sortedMethods = new SortedDictionary<int, List<TTestCase>>();
foreach (TTestCase testCase in testCases)
{
int priority = testCase.TestMethod.Method
.GetCustomAttributes(assemblyName)
.FirstOrDefault()
?.GetNamedArgument<int>(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<TKey, TValue>(
IDictionary<TKey, TValue> dictionary, TKey key)
where TKey : struct
where TValue : new() =>
dictionary.TryGetValue(key, out TValue? result)
? result
: (dictionary[key] = new TValue());
}

View file

@ -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;
}