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 values) : AnalyzerConfigOptions { public override bool TryGetValue(string key, out string value) => values.TryGetValue(key, out value!); } private sealed class FakeOptionsProvider(Dictionary 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? options = null) { var driver = CSharpGeneratorDriver.Create( [new AssetHandlesGenerator().AsSourceGenerator()], additionalTexts: Array.ConvertAll(files, f => (AdditionalText)new FakeAdditionalText(f)), optionsProvider: new FakeOptionsProvider(options ?? new Dictionary { ["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 Player = new(\"Textures/player.png\")", source); Assert.Contains( "AssetRef Jump = new(\"Sounds/jump.wav\")", source); Assert.Contains("AssetRef Main", source); Assert.Contains("AssetRef 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 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(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)); } }