Files
Leonid Pershin fd6343bd09
CI / build-test (push) Failing after 1m8s
@
Add MrGameEng.AI utility-AI module; format codebase with CSharpier

New MrGameEng.AI module (ResponseCurve, Consideration, UtilityAction,
UtilityAi selector, Blackboard) plus CSharpier formatting applied across
the whole engine. Documents the CSharpier convention in CLAUDE.md.
@
2026-06-12 07:19:10 +03:00

218 lines
8.0 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using MrGameEng.Assets.Generator;
using Xunit;
namespace MrGameEng.Assets.Generator.Tests;
public class AssetHandlesGeneratorTests
{
private sealed class FakeAdditionalText(string path) : AdditionalText
{
public override string Path { get; } = path;
public override SourceText GetText(CancellationToken cancellationToken = default) =>
SourceText.From(string.Empty);
}
private sealed class FakeOptions(Dictionary<string, string> values) : AnalyzerConfigOptions
{
public override bool TryGetValue(string key, out string value) =>
values.TryGetValue(key, out value!);
}
private sealed class FakeOptionsProvider(Dictionary<string, string> values)
: AnalyzerConfigOptionsProvider
{
public override AnalyzerConfigOptions GlobalOptions { get; } = new FakeOptions(values);
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => GlobalOptions;
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => GlobalOptions;
}
private static string RunGenerator(string[] files, Dictionary<string, string>? options = null)
{
var driver = CSharpGeneratorDriver.Create(
[new AssetHandlesGenerator().AsSourceGenerator()],
additionalTexts: Array.ConvertAll(
files,
f => (AdditionalText)new FakeAdditionalText(f)
),
optionsProvider: new FakeOptionsProvider(
options
?? new Dictionary<string, string>
{
["build_property.RootNamespace"] = "MyGame",
}
)
);
var compilation = CSharpCompilation.Create("test");
var result = driver.RunGenerators(compilation).GetRunResult();
return Assert.Single(Assert.Single(result.Results).GeneratedSources).SourceText.ToString();
}
[Fact]
public void GeneratesTypedHandles_ForKnownExtensions()
{
var source = RunGenerator([
@"D:\game\Assets\Textures\player.png",
@"D:\game\Assets\Sounds\jump.wav",
@"D:\game\Assets\Fonts\main.ttf",
@"D:\game\Assets\Music\theme.ogg",
]);
Assert.Contains("namespace MyGame;", source);
Assert.Contains("public static partial class GameAssets", source);
Assert.Contains("public static class Textures", source);
Assert.Contains(
"AssetRef<global::Microsoft.Xna.Framework.Graphics.Texture2D> Player = new(\"Textures/player.png\")",
source
);
Assert.Contains(
"AssetRef<global::Microsoft.Xna.Framework.Audio.SoundEffect> Jump = new(\"Sounds/jump.wav\")",
source
);
Assert.Contains("AssetRef<global::FontStashSharp.FontSystem> Main", source);
Assert.Contains("AssetRef<global::MrGameEng.Core.MusicTrack> Theme", source);
}
[Fact]
public void IgnoresUnknownExtensions_AndFilesOutsideAssets()
{
var source = RunGenerator([
@"D:\game\Assets\readme.md",
@"D:\game\Other\image.png",
@"D:\game\Assets\valid.png",
]);
Assert.Contains("Valid", source);
Assert.DoesNotContain("Readme", source);
Assert.DoesNotContain("Image", source);
}
[Fact]
public void AtlasFiles_GetTextureAtlasHandles_AndPagesAreExcluded()
{
var source = RunGenerator([
@"D:\game\Assets\Atlases\Things.Pawn.atlas",
@"D:\game\Assets\Atlases\Things.Pawn.atlas.0.png",
@"D:\game\Assets\Atlases\Things.Pawn.atlas.1.png",
]);
Assert.Contains(
"AssetRef<global::MrGameEng.Atlases.TextureAtlas> ThingsPawn = new(\"Atlases/Things.Pawn.atlas\")",
source
);
Assert.DoesNotContain("Texture2D> ThingsPawn", source);
}
[Fact]
public void NestedDirectories_BecomeNestedClasses()
{
var source = RunGenerator([@"C:\proj\Assets\UI\Icons\save-icon.png"]);
Assert.Contains("public static class UI", source);
Assert.Contains("public static class Icons", source);
Assert.Contains("SaveIcon = new(\"UI/Icons/save-icon.png\")", source);
}
[Fact]
public void GeneratedCode_Compiles()
{
var source = RunGenerator([@"D:\game\Assets\player.png"]);
// Подменяем внешние типы заглушками, чтобы скомпилировать сгенерированный код изолированно.
const string stubs = """
namespace MrGameEng.Assets { public readonly record struct AssetRef<T>(string Path) where T : class; }
namespace Microsoft.Xna.Framework.Graphics { public sealed class Texture2D; }
""";
var compilation = CSharpCompilation.Create(
"generated",
[
CSharpSyntaxTree.ParseText(
source,
cancellationToken: TestContext.Current.CancellationToken
),
CSharpSyntaxTree.ParseText(
stubs,
cancellationToken: TestContext.Current.CancellationToken
),
],
[
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(
System
.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyName(
new System.Reflection.AssemblyName("System.Runtime")
)
.Location
),
],
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
var errors = compilation
.GetDiagnostics(TestContext.Current.CancellationToken)
.Where(d => d.Severity == DiagnosticSeverity.Error)
.ToList();
Assert.Empty(errors);
}
[Theory]
[InlineData("player", "Player")]
[InlineData("save-icon", "SaveIcon")]
[InlineData("8bit_font", "_8bitFont")]
[InlineData("...", "_")]
public void Identifier_SanitizesNames(string input, string expected)
{
Assert.Equal(expected, AssetHandlesGenerator.Identifier(input));
}
[Theory]
[InlineData(@"D:\game\Assets\a.png", "a.png")]
[InlineData("/home/user/game/Assets/sub/b.wav", "sub/b.wav")]
[InlineData(@"D:\game\NotAssets\c.png", null)]
[InlineData(@"D:\game\Assets\unknown.xyz", null)]
public void ToAssetPath_ExtractsRelativePath(string fullPath, string? expected)
{
Assert.Equal(expected, AssetHandlesGenerator.ToAssetPath(fullPath));
}
[Theory]
[InlineData(@"D:\game\Assets\ui\Assets\icon.png", @"D:\game", "ui/Assets/icon.png")]
[InlineData(@"D:\game\Assets\icon.png", @"D:\game\", "icon.png")]
[InlineData(@"D:\game\Other\icon.png", @"D:\game", null)]
public void ToAssetPath_WithProjectDir_RootsAtProjectAssetsFolder(
string fullPath,
string projectDir,
string? expected
)
{
Assert.Equal(expected, AssetHandlesGenerator.ToAssetPath(fullPath, projectDir));
}
[Fact]
public void SpecialCharactersInNames_AreEscapedInDocsAndLiterals()
{
var source = RunGenerator([@"D:\game\Assets\UI\a&b <c>.png"]);
Assert.Contains("a&amp;b &lt;c&gt;", source); // XML-док экранирован (иначе CS1570)
Assert.Contains("new(\"UI/a&b <c>.png\")", source);
}
[Fact]
public void FileNamedLikeContainerClass_DoesNotCollide()
{
// Член с именем вмещающего класса — ошибка CS0542; генератор должен переименовать.
var source = RunGenerator([@"D:\game\Assets\GameAssets.png"]);
Assert.Contains("GameAssets2 = new(\"GameAssets.png\")", source);
Assert.DoesNotContain("GameAssets = new(", source);
}
}