extern alias SerializationGenerator; using System.Linq; using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using NUnit.Framework; using SerializationGenerator::Robust.Roslyn.Shared; using SerializationGenerator::Robust.Serialization.Generator; namespace Robust.Analyzers.Tests; [TestFixture] [TestOf(typeof(ComponentPauseGenerator))] [Parallelizable(ParallelScope.All)] public sealed class ComponentPauseGeneratorTest { private const string TypesCode = """ global using System; global using Robust.Shared.Analyzers; global using Robust.Shared.GameObjects; namespace Robust.Shared.Analyzers { [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class AutoGenerateComponentPauseAttribute : Attribute { public bool Dirty = false; } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public sealed class AutoPausedFieldAttribute : Attribute; [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public sealed class AutoNetworkedFieldAttribute : Attribute { } } namespace Robust.Shared.GameObjects { public interface IComponent; } """; [Test] public void TestBasic() { var result = RunGenerator(""" [AutoGenerateComponentPause] public sealed partial class FooComponent : IComponent { [AutoPausedField] public TimeSpan Foo; } """); ExpectNoDiagnostics(result); ExpectSource( result, """ // using Robust.Shared.GameObjects; public partial class FooComponent { [RobustAutoGenerated] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] public sealed class FooComponent_AutoPauseSystem : EntitySystem { public override void Initialize() { SubscribeLocalEvent(OnEntityUnpaused); } private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args) { component.Foo += args.PausedTime; } } } """); } [Test] public void TestNullable() { var result = RunGenerator(""" [AutoGenerateComponentPause] public sealed partial class FooComponent : IComponent { [AutoPausedField] public TimeSpan? Foo; } """); ExpectNoDiagnostics(result); ExpectSource( result, """ // using Robust.Shared.GameObjects; public partial class FooComponent { [RobustAutoGenerated] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] public sealed class FooComponent_AutoPauseSystem : EntitySystem { public override void Initialize() { SubscribeLocalEvent(OnEntityUnpaused); } private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args) { if (component.Foo.HasValue) component.Foo = component.Foo.Value + args.PausedTime; } } } """); } [Test] public void TestDictionary() { var result = RunGenerator(""" [AutoGenerateComponentPause] public sealed partial class FooComponent : IComponent { [AutoPausedField] public Dictionary Foo; } """); ExpectNoDiagnostics(result); ExpectSource( result, """ // using Robust.Shared.GameObjects; public partial class FooComponent { [RobustAutoGenerated] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] public sealed class FooComponent_AutoPauseSystem : EntitySystem { public override void Initialize() { SubscribeLocalEvent(OnEntityUnpaused); } private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args) { foreach (var key in component.Foo.Keys) component.Foo[key] += args.PausedTime; } } } """); } [Test] public void TestAutoState() { var result = RunGenerator(""" [AutoGenerateComponentPause] public sealed partial class FooComponent : IComponent { [AutoPausedField, AutoNetworkedField] public TimeSpan Foo; } """); ExpectNoDiagnostics(result); ExpectSource( result, """ // using Robust.Shared.GameObjects; public partial class FooComponent { [RobustAutoGenerated] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] public sealed class FooComponent_AutoPauseSystem : EntitySystem { public override void Initialize() { SubscribeLocalEvent(OnEntityUnpaused); } private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args) { component.Foo += args.PausedTime; Dirty(uid, component); } } } """); } [Test] public void TestExplicitDirty() { var result = RunGenerator(""" [AutoGenerateComponentPause(Dirty = true)] public sealed partial class FooComponent : IComponent { [AutoPausedField] public TimeSpan Foo; } """); ExpectNoDiagnostics(result); ExpectSource( result, """ // using Robust.Shared.GameObjects; public partial class FooComponent { [RobustAutoGenerated] [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] public sealed class FooComponent_AutoPauseSystem : EntitySystem { public override void Initialize() { SubscribeLocalEvent(OnEntityUnpaused); } private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args) { component.Foo += args.PausedTime; Dirty(uid, component); } } } """); } [Test] public void TestDiagnosticNotIComponent() { var result = RunGenerator(""" [AutoGenerateComponentPause] public sealed partial class FooComponent { [AutoPausedField] public TimeSpan Foo; } """); ExpectNoSource(result); ExpectDiagnostics(result, [ (Diagnostics.IdComponentPauseNotComponent, new LinePositionSpan(new LinePosition(1, 28), new LinePosition(1, 40))) ]); } [Test] public void TestDiagnosticNoFields() { var result = RunGenerator(""" [AutoGenerateComponentPause] public sealed partial class FooComponent : IComponent { public TimeSpan Foo; } """); ExpectNoSource(result); ExpectDiagnostics(result, [ (Diagnostics.IdComponentPauseNoFields, new LinePositionSpan(new LinePosition(1, 28), new LinePosition(1, 40))) ]); } [Test] public void TestDiagnosticNoParentAttribute() { var result = RunGenerator(""" public sealed partial class FooComponent : IComponent { [AutoPausedField] public TimeSpan Foo, Fooz; [AutoPausedField] public TimeSpan Bar { get; set; } } """); ExpectNoSource(result); ExpectDiagnostics(result, [ (Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(3, 20), new LinePosition(3, 23))), (Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(3, 25), new LinePosition(3, 29))), (Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(6, 20), new LinePosition(6, 23))) ]); } [Test] public void TestDiagnosticWrongType() { var result = RunGenerator(""" [AutoGenerateComponentPause] public sealed partial class FooComponent : IComponent { [AutoPausedField] public int Foo, Fooz; [AutoPausedField] public int Bar { get; set; } } """); ExpectNoSource(result); ExpectDiagnostics(result, [ (Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(4, 15), new LinePosition(4, 18))), (Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(4, 20), new LinePosition(4, 24))), (Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(7, 15), new LinePosition(7, 18))) ]); } private static void ExpectSource(GeneratorRunResult result, string expected) { Assert.That(result.GeneratedSources, Has.Length.EqualTo(1)); var source = result.GeneratedSources[0]; Assert.That(source.SourceText.ToString(), Is.EqualTo(expected)); } private static void ExpectNoSource(GeneratorRunResult result) { Assert.That(result.GeneratedSources, Is.Empty); } private static void ExpectNoDiagnostics(GeneratorRunResult result) { Assert.That(result.Diagnostics, Is.Empty); } private static void ExpectDiagnostics(GeneratorRunResult result, (string code, LinePositionSpan span)[] diagnostics) { Assert.Multiple(() => { Assert.That(result.Diagnostics, Has.Length.EqualTo(diagnostics.Length)); foreach (var (code, span) in diagnostics) { Assert.That( result.Diagnostics.Any(x => x.Id == code && x.Location.GetLineSpan().Span == span), $"Expected diagnostic with code {code} and location {span}"); } }); } private static GeneratorRunResult RunGenerator(string source) { var compilation = (Compilation)CSharpCompilation.Create("compilation", new[] { CSharpSyntaxTree.ParseText(source, path: "Source.cs"), CSharpSyntaxTree.ParseText(TypesCode, path: "Types.cs") }, new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) }, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); var generator = new ComponentPauseGenerator(); GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out _); var result = driver.GetRunResult(); return result.Results[0]; } }