mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
* Source gen reorganizations + component unpause generator. This commit (and subsequent commits) aims to clean up our Roslyn plugin (source gens + analyzers) stack to more sanely re-use common code I also built a new source-gen that automatically generates unpausing implementations for components, incrementing attributed TimeSpan field when unpaused. * Fix warnings in all Roslyn projects
341 lines
10 KiB
C#
341 lines
10 KiB
C#
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,
|
|
"""
|
|
// <auto-generated />
|
|
|
|
using Robust.Shared.GameObjects;
|
|
|
|
public partial class FooComponent
|
|
{
|
|
[RobustAutoGenerated]
|
|
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
|
{
|
|
public override void Initialize()
|
|
{
|
|
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(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,
|
|
"""
|
|
// <auto-generated />
|
|
|
|
using Robust.Shared.GameObjects;
|
|
|
|
public partial class FooComponent
|
|
{
|
|
[RobustAutoGenerated]
|
|
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
|
{
|
|
public override void Initialize()
|
|
{
|
|
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(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 TestAutoState()
|
|
{
|
|
var result = RunGenerator("""
|
|
[AutoGenerateComponentPause]
|
|
public sealed partial class FooComponent : IComponent
|
|
{
|
|
[AutoPausedField, AutoNetworkedField]
|
|
public TimeSpan Foo;
|
|
}
|
|
""");
|
|
|
|
ExpectNoDiagnostics(result);
|
|
ExpectSource(
|
|
result,
|
|
"""
|
|
// <auto-generated />
|
|
|
|
using Robust.Shared.GameObjects;
|
|
|
|
public partial class FooComponent
|
|
{
|
|
[RobustAutoGenerated]
|
|
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
|
{
|
|
public override void Initialize()
|
|
{
|
|
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(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,
|
|
"""
|
|
// <auto-generated />
|
|
|
|
using Robust.Shared.GameObjects;
|
|
|
|
public partial class FooComponent
|
|
{
|
|
[RobustAutoGenerated]
|
|
public sealed class FooComponent_AutoPauseSystem : EntitySystem
|
|
{
|
|
public override void Initialize()
|
|
{
|
|
SubscribeLocalEvent<FooComponent, EntityUnpausedEvent>(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];
|
|
}
|
|
}
|