mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
fix(config): 修复Godot YAML配置加载器的目录重置异常处理
- 为构造函数添加ArgumentNullException和ArgumentException异常说明 - 为EnableHotReload方法添加InvalidOperationException异常说明 - 重构ResetDirectory方法以捕获目录操作异常并包装为ConfigLoadException - 添加detail参数到CreateConfigLoadException方法用于提供更详细的错误信息 - 新增单元测试验证运行时缓存目录重置失败时的异常处理 - 添加GodotYamlConfigTableSourceTests测试类验证安全相对路径约束
This commit is contained in:
parent
e746297496
commit
1bf5d287e9
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using GFramework.Game.Abstractions.Config;
|
||||
using GFramework.Game.Config;
|
||||
using GFramework.Godot.Config;
|
||||
using NUnit.Framework;
|
||||
@ -176,6 +177,31 @@ public sealed class GodotYamlConfigLoaderTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证运行时缓存目录无法重置时,Godot 适配层仍会返回结构化的配置加载诊断。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LoadAsync_Should_Wrap_Runtime_Cache_Directory_Reset_Failure_As_ConfigLoadException()
|
||||
{
|
||||
CreateMonsterFiles(_resourceRoot);
|
||||
WriteFile(_userRoot, "config_cache", "occupied");
|
||||
|
||||
var loader = CreateLoader(isEditor: false);
|
||||
|
||||
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () =>
|
||||
await loader.LoadAsync(new ConfigRegistry()));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(exception, Is.Not.Null);
|
||||
Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.ConfigFileReadFailed));
|
||||
Assert.That(exception.Diagnostic.TableName, Is.EqualTo("monster"));
|
||||
Assert.That(exception.Diagnostic.ConfigDirectoryPath, Is.EqualTo(Path.Combine(_resourceRoot, "monster")));
|
||||
Assert.That(exception.Diagnostic.Detail, Does.Contain(Path.Combine(_userRoot, "config_cache", "monster")));
|
||||
Assert.That(exception.InnerException, Is.InstanceOf<IOException>());
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证加载器自身会拒绝可能逃逸缓存根目录的非法配置目录路径,即使调用方绕过了公开构造约束。
|
||||
/// </summary>
|
||||
|
||||
@ -15,11 +15,16 @@ public sealed class GodotYamlConfigTableSourceTests
|
||||
/// </summary>
|
||||
/// <param name="configRelativePath">待验证的配置目录路径。</param>
|
||||
[TestCase("../outside")]
|
||||
[TestCase(@"..\outside")]
|
||||
[TestCase("./monster")]
|
||||
[TestCase(@".\monster")]
|
||||
[TestCase("monster/../outside")]
|
||||
[TestCase(@"monster\..\outside")]
|
||||
[TestCase("monster/./child")]
|
||||
[TestCase(@"monster\.\child")]
|
||||
[TestCase("/monster")]
|
||||
[TestCase("C:/monster")]
|
||||
[TestCase(@"C:\monster")]
|
||||
[TestCase("res://monster")]
|
||||
[TestCase("user://monster")]
|
||||
public void Constructor_Should_Throw_When_Config_Relative_Path_Is_Not_Safe(string configRelativePath)
|
||||
@ -35,11 +40,16 @@ public sealed class GodotYamlConfigTableSourceTests
|
||||
/// </summary>
|
||||
/// <param name="schemaRelativePath">待验证的 schema 路径。</param>
|
||||
[TestCase("../schemas/monster.schema.json")]
|
||||
[TestCase(@"..\schemas\monster.schema.json")]
|
||||
[TestCase("./schemas/monster.schema.json")]
|
||||
[TestCase(@".\schemas\monster.schema.json")]
|
||||
[TestCase("schemas/../monster.schema.json")]
|
||||
[TestCase(@"schemas\..\monster.schema.json")]
|
||||
[TestCase("schemas/./monster.schema.json")]
|
||||
[TestCase(@"schemas\.\monster.schema.json")]
|
||||
[TestCase("/schemas/monster.schema.json")]
|
||||
[TestCase("C:/schemas/monster.schema.json")]
|
||||
[TestCase(@"C:\schemas\monster.schema.json")]
|
||||
[TestCase("res://schemas/monster.schema.json")]
|
||||
[TestCase("user://schemas/monster.schema.json")]
|
||||
public void Constructor_Should_Throw_When_Schema_Relative_Path_Is_Not_Safe(string schemaRelativePath)
|
||||
|
||||
@ -22,6 +22,20 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
||||
/// 使用指定选项创建一个 Godot YAML 配置加载器。
|
||||
/// </summary>
|
||||
/// <param name="options">加载器初始化选项。</param>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="options" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// 当 <see cref="GodotYamlConfigLoaderOptions.SourceRootPath" /> 或
|
||||
/// <see cref="GodotYamlConfigLoaderOptions.RuntimeCacheRootPath" /> 为空白字符串时抛出。
|
||||
/// </exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 当 Godot 特殊路径无法被全局化为非空绝对路径时抛出。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 构造完成后,加载器会根据当前环境决定直接读取 <see cref="SourceRootPath" />,还是先同步到
|
||||
/// <see cref="RuntimeCacheRootPath" /> 再交给底层 <see cref="YamlConfigLoader" />。
|
||||
/// 只有源根目录可直接作为普通文件系统目录访问时,<see cref="CanEnableHotReload" /> 才会返回
|
||||
/// <see langword="true" />。
|
||||
/// </remarks>
|
||||
public GodotYamlConfigLoader(GodotYamlConfigLoaderOptions options)
|
||||
: this(options, GodotYamlConfigEnvironment.Default)
|
||||
{
|
||||
@ -115,6 +129,19 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
||||
/// <param name="registry">要被热重载更新的配置注册表。</param>
|
||||
/// <param name="options">热重载选项;为空时使用默认值。</param>
|
||||
/// <returns>用于停止监听的注销句柄。</returns>
|
||||
/// <exception cref="ArgumentNullException">当 <paramref name="registry" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// 当当前实例必须通过运行时缓存访问配置源,无法直接监听真实源目录时抛出。
|
||||
/// </exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// 当 <paramref name="options" /> 的防抖延迟小于 <see cref="TimeSpan.Zero" /> 时,
|
||||
/// 底层 <see cref="YamlConfigLoader" /> 会拒绝启用热重载。
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// 调用前应先检查 <see cref="CanEnableHotReload" />。
|
||||
/// 当 <see cref="SourceRootPath" /> 只能通过缓存同步访问时,拒绝启用热重载是为了避免监听缓存副本后误导调用方,
|
||||
/// 让其误以为源目录改动会被自动反映到运行时。
|
||||
/// </remarks>
|
||||
public IUnRegister EnableHotReload(
|
||||
IConfigRegistry registry,
|
||||
YamlConfigHotReloadOptions? options = null)
|
||||
@ -171,7 +198,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
||||
var sourceDirectoryPath = CombinePath(SourceRootPath, representative.ConfigRelativePath);
|
||||
var targetDirectoryPath = CombineAbsolutePath(LoaderRootPath, representative.ConfigRelativePath);
|
||||
|
||||
ResetDirectory(targetDirectoryPath);
|
||||
ResetDirectory(representative.TableName, sourceDirectoryPath, targetDirectoryPath);
|
||||
CopyYamlFilesInDirectory(
|
||||
representative.TableName,
|
||||
sourceDirectoryPath,
|
||||
@ -215,8 +242,6 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
||||
configDirectoryPath: DescribePath(sourceDirectoryPath));
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetDirectoryPath);
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@ -303,14 +328,28 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetDirectory(string directoryPath)
|
||||
private void ResetDirectory(string tableName, string sourceDirectoryPath, string targetDirectoryPath)
|
||||
{
|
||||
if (Directory.Exists(directoryPath))
|
||||
try
|
||||
{
|
||||
Directory.Delete(directoryPath, recursive: true);
|
||||
}
|
||||
if (Directory.Exists(targetDirectoryPath))
|
||||
{
|
||||
Directory.Delete(targetDirectoryPath, recursive: true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(directoryPath);
|
||||
Directory.CreateDirectory(targetDirectoryPath);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
var describedSourceDirectoryPath = DescribePath(sourceDirectoryPath);
|
||||
throw CreateConfigLoadException(
|
||||
ConfigLoadFailureKind.ConfigFileReadFailed,
|
||||
tableName,
|
||||
$"Failed to reset runtime cache directory '{targetDirectoryPath}' while preparing config directory '{describedSourceDirectoryPath}'.",
|
||||
configDirectoryPath: describedSourceDirectoryPath,
|
||||
detail: $"Runtime cache directory: {targetDirectoryPath}.",
|
||||
innerException: exception);
|
||||
}
|
||||
}
|
||||
|
||||
private string EnsureAbsolutePath(string path, string optionName)
|
||||
@ -417,6 +456,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
||||
string? configDirectoryPath = null,
|
||||
string? yamlPath = null,
|
||||
string? schemaPath = null,
|
||||
string? detail = null,
|
||||
Exception? innerException = null)
|
||||
{
|
||||
return new ConfigLoadException(
|
||||
@ -425,7 +465,8 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
||||
tableName,
|
||||
configDirectoryPath: configDirectoryPath,
|
||||
yamlPath: yamlPath,
|
||||
schemaPath: schemaPath),
|
||||
schemaPath: schemaPath,
|
||||
detail: detail),
|
||||
message,
|
||||
innerException);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user