A:common 文件测试

This commit is contained in:
jimmy committed 2025-11-13 18:51:31 +08:00
1 parent 9cfbc3e458
commit fdf9a3c762
7 files changed
+2610

No files matched your search

+284
View File
@@ -0,0 +1,284 @@
using Microsoft.Extensions.Configuration;
using System.ComponentModel.DataAnnotations;
using System.Text;
// - 本文件覆盖 Config 模块的核心能力测试:多格式读写、强类型绑定、验证、环境变量、热更新、Hook 扩展。
// - 使用 MSTest。请确保已在测试项目中添加对 src\Sharp\Config\Config.csproj 的 ProjectReference。
// - 约定:配置项的“后添加覆盖先添加”(Configuration 的默认行为)。环境变量位于最末,优先级最高。
namespace Config.Test
{
// 为避免文件监听与并发写入导致的互相影响,禁用并行执行
[TestClass]
[DoNotParallelize]
public class ConfigTests
{
// 简单的配置模型,涵盖:基础字段、嵌套对象、DataAnnotations 验证
public class AppSettings
{
public string? Name { get; set; }
public int Port { get; set; }
[Range(1, 65535)] public int ValidPort { get; set; }
[Required] public string? RequiredField { get; set; }
public DatabaseSettings Database { get; set; } = new();
}
public class DatabaseSettings
{
[Required] public string? Host { get; set; }
public string? User { get; set; }
public string? Password { get; set; }
}
// 创建临时目录的轻量封装,便于测试隔离
private sealed class TempDirScope : IDisposable
{
public string Dir { get; }
public TempDirScope()
{
Dir = Path.Combine(Path.GetTempPath(), "ConfigTests_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Dir);
}
public string PathOf(string file) => System.IO.Path.Combine(Dir, file);
public void Dispose()
{
try
{
Directory.Delete(Dir, recursive: true);
}
catch
{
/* 文件监视可能暂占用,忽略 */
}
}
}
[TestMethod]
public void Build_FromFiles_Binds_Json_Ini_Xml_Toml_WithValidation()
{
using var scope = new TempDirScope();
// 1. 构造多格式配置文件,分别写入不同字段
var jsonPath = scope.PathOf("appsettings.json");
var iniPath = scope.PathOf("settings.ini");
var xmlPath = scope.PathOf("extra.xml");
var tomlPath = scope.PathOf("feature.toml");
File.WriteAllText(jsonPath, /* UTF-8 无 BOM */
"{\n" +
" \"Name\": \"fromJson\",\n" +
" \"Port\": 10\n" +
"}", new UTF8Encoding(false));
File.WriteAllText(iniPath,
// INI 文件:根键与一层嵌套(Database 节)
"User=iniUser\n" +
"[Database]\n" +
"Host=iniHost\n",
new UTF8Encoding(false));
File.WriteAllText(xmlPath,
// XML:顶层键直接映射
"<settings>\n" +
" <ValidPort>100</ValidPort>\n" +
"</settings>", new UTF8Encoding(false));
File.WriteAllText(tomlPath,
"RequiredField = \"fromToml\"\n",
new UTF8Encoding(false));
// 2. 构建配置(顺序:JSON -> INI -> XML -> TOML),最后添加环境变量
var cm = ConfigBuilder.BuildFromFiles(
files: new[] { "appsettings.json", "settings.ini", "extra.xml", "feature.toml" },
reloadOnChange: true,
includeEnvironment: true,
environmentPrefix: "APP_",
basePath: scope.Dir
);
// 3. 强类型绑定 + 验证(DataAnnotations)
var settings = cm.Get<AppSettings>(validate: true);
Assert.AreEqual("fromJson", settings.Name);
Assert.AreEqual(10, settings.Port);
Assert.AreEqual(100, settings.ValidPort);
Assert.AreEqual("iniHost", settings.Database.Host);
Assert.AreEqual("iniUser", settings.Database.User);
Assert.AreEqual("fromToml", settings.RequiredField);
}
[TestMethod]
public void Validation_Fails_On_Missing_Required()
{
using var scope = new TempDirScope();
// 缺少 RequiredField 与 Database.Host,期望验证失败
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"x\", \"Port\": 5, \"ValidPort\": 200 }",
new UTF8Encoding(false));
var cm = ConfigBuilder.BuildFromFiles(new[] { "appsettings.json" }, basePath: scope.Dir);
Assert.ThrowsException<ConfigurationValidationException>(() => { cm.Get<AppSettings>(validate: true); });
}
[TestMethod]
public void EnvironmentVariables_Override_FileValues()
{
using var scope = new TempDirScope();
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"fromFile\", \"Database\": { \"Host\": \"fileHost\" } }",
new UTF8Encoding(false));
// 使用前缀 APP_,配置键用双下划线分隔层级
Environment.SetEnvironmentVariable("APP__Name", "fromEnv");
Environment.SetEnvironmentVariable("APP__Database__Host", "envHost");
try
{
var cm = ConfigBuilder.BuildFromFiles(
new[] { "appsettings.json" },
basePath: scope.Dir,
includeEnvironment: true,
environmentPrefix: "APP_"
);
var settings = cm.Get<AppSettings>();
Assert.AreEqual("fromEnv", settings.Name);
Assert.AreEqual("envHost", settings.Database.Host);
}
finally
{
// 清理环境变量,避免影响其他测试
Environment.SetEnvironmentVariable("APP__Name", null);
Environment.SetEnvironmentVariable("APP__Database__Host", null);
}
}
[TestMethod]
[Timeout(15000)]
public async Task HotReload_FileChange_Triggers_Callback_And_Rebind()
{
using var scope = new TempDirScope();
var json = scope.PathOf("hot.json");
File.WriteAllText(json, "{ \"Port\": 1 }", new UTF8Encoding(false));
var cm = ConfigBuilder.BuildFromFiles(
new[] { "hot.json" },
reloadOnChange: true,
basePath: scope.Dir
);
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using var sub = cm.OnReload(() => tcs.TrySetResult(true));
// 初始读取
var s1 = cm.Get<AppSettings>();
Assert.AreEqual(1, s1.Port);
// 修改内容,触发文件监控
await Task.Delay(250); // 稍等,确保监控挂上
File.WriteAllText(json, "{ \"Port\": 2 }", new UTF8Encoding(false));
// 等待回调触发
var fired = await Task.WhenAny(tcs.Task, Task.Delay(5000)) == tcs.Task;
Assert.IsTrue(fired, "未在期望时间内触发热更新回调");
// 变更后再取配置
var s2 = cm.Get<AppSettings>();
Assert.AreEqual(2, s2.Port);
}
[TestMethod]
public void Save_And_Load_Json_Xml_Ini_Toml()
{
using var scope = new TempDirScope();
var settings = new AppSettings
{
Name = "save",
Port = 1234,
ValidPort = 80,
RequiredField = "req",
Database = new DatabaseSettings { Host = "db", User = "u" }
};
var cm = ConfigManager.Build(new ConfigOptions()); // 用于调用 Save/Load
// JSON
var json = scope.PathOf("a.json");
cm.Save(settings, json, ConfigFormat.Json);
var jsonBack = cm.LoadFromFile<AppSettings>(json, ConfigFormat.Json);
Assert.AreEqual("save", jsonBack!.Name);
Assert.AreEqual(1234, jsonBack.Port);
Assert.AreEqual("db", jsonBack.Database.Host);
// XML
var xml = scope.PathOf("a.xml");
cm.Save(settings, xml, ConfigFormat.Xml);
var xmlBack = cm.LoadFromFile<AppSettings>(xml, ConfigFormat.Xml);
Assert.AreEqual(80, xmlBack!.ValidPort);
// INI(支持简单与一层嵌套的序列化回读)
var ini = scope.PathOf("a.ini");
cm.Save(settings, ini, ConfigFormat.Ini);
var iniBack = cm.LoadFromFile<AppSettings>(ini, ConfigFormat.Ini);
Assert.AreEqual("u", iniBack!.Database.User);
Assert.AreEqual("db", iniBack.Database.Host);
// TOML:当前不支持直接泛型反序列化(LoadFromFile),需通过配置管线加载
var toml = scope.PathOf("a.toml");
cm.Save(settings, toml, ConfigFormat.Toml);
Assert.ThrowsException<NotSupportedException>(() =>
{
_ = cm.LoadFromFile<AppSettings>(toml, ConfigFormat.Toml);
});
var cmToml = ConfigBuilder.BuildFromFiles(new[] { "a.toml" }, basePath: scope.Dir);
var tomlBack = cmToml.Get<AppSettings>();
Assert.AreEqual("save", tomlBack.Name);
Assert.AreEqual("db", tomlBack.Database.Host);
}
[TestMethod]
public void ConfigureBuilderHook_Appends_Providers_And_Overrides()
{
using var scope = new TempDirScope();
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"fileName\" }",
new UTF8Encoding(false));
var cm = ConfigBuilder.Build(opt =>
{
opt.BasePath = scope.Dir;
opt.Sources.Add(new ConfigSource
{
Path = "appsettings.json", Format = ConfigFormat.Json, Optional
= false
});
// 通过 Hook 追加内存配置,位于最后,覆盖前面的值
opt.ConfigureBuilderHook = builder =>
{
var kv = new[] { new KeyValuePair<string, string?>("Name", "hookName") };
builder.AddInMemoryCollection(kv);
return builder;
};
});
var s = cm.Get<AppSettings>();
Assert.AreEqual("hookName", s.Name, "Hook 未按预期覆盖前序配置源");
}
}
}