Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<PackageId>ModernUO.Serialization.Annotations</PackageId>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
<AssemblyVersion>4.1.1</AssemblyVersion>
<PackageVersion>4.1.1</PackageVersion>
<AssemblyVersion>4.2.0</AssemblyVersion>
<PackageVersion>4.2.0</PackageVersion>
<AssemblyName>ModernUO.Serialization.Annotations</AssemblyName>
<RootNamespace>ModernUO.Serialization</RootNamespace>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
Expand Down
98 changes: 98 additions & 0 deletions ModernUO.Serialization.Generator.Tests/DataStructureMethodTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.Runtime.CompilerServices;
using ModernUO.Serialization.Generator.Tests.Helpers;
using Xunit;

namespace ModernUO.Serialization.Generator.Tests;

/// <summary>
/// Runs the generated AddToX/RemoveFromX/InsertIntoX/ReplaceInX/ClearX helpers against the
/// Snapshots/NullableCollections entity: removals and clears tolerate a null collection, adds create
/// it lazily (keeping a [SortedSetComparer]), and MarkDirty fires only when the collection changed.
/// </summary>
public class DataStructureMethodTests
{
private static string FixtureSource([CallerFilePath] string thisFile = "") =>
File.ReadAllText(
Path.Combine(Path.GetDirectoryName(thisFile)!, "Snapshots", "NullableCollections", "Input.cs")
);

private const string Driver = """
namespace Server.TestContent
{
public static class Driver
{
public static string Run()
{
var item = new NullableCollectionsItem();
var log = new System.Text.StringBuilder();
void Step(string name, object? state = null) =>
log.Append(name).Append('=').Append(item.DirtyCount).Append(state != null ? $":{state}" : "").Append(';');

item.RemoveFromCharges(1);
item.RemoveFromChargesAt(0);
item.ClearCharges();
item.RemoveFromKeywords("a");
item.ClearKeywords();
item.RemoveFromLabels(1);
item.ClearLabels();
item.RemoveFromNames("a");
item.ClearNames();
Step("null", item.Charges == null && item.Keywords == null && item.Labels == null && item.Names == null);

item.AddToCharges(5);
Step("addList");
item.InsertIntoCharges(0, 4);
Step("insert", string.Join(",", item.Charges!));

item.AddToKeywords("a");
Step("addSet");
item.AddToKeywords("a");
Step("addSetDuplicate");

item.AddToLabels(1, "x");
Step("addDictionary");
item.AddToLabels(1, "y");
Step("addDictionaryDuplicate", item.Labels![1]);
item.ReplaceInLabels(1, null);
Step("replace", item.Labels[1] ?? "null");

item.AddToNames("Bob");
item.AddToNames("BOB");
Step("comparer", item.Names!.Count);

item.RemoveFromKeywords("missing");
Step("removeMissing");
item.RemoveFromKeywords("a");
Step("remove");
item.ClearKeywords();
Step("clearEmpty");
item.ClearCharges();
Step("clear", item.Charges.Count);

var fresh = new NullableCollectionsItem();
fresh.ReplaceInLabels(2, "z");
log.Append("replaceNull=").Append(fresh.DirtyCount).Append(':').Append(fresh.Labels![2]);

return log.ToString();
}
}
}
""";

[Fact]
public void Helpers_TolerateNullAndMarkDirtyOnlyOnChange()
{
var assembly = SourceGeneratorTestHelper.CompileAndLoad("NullableCollections", FixtureSource() + Driver);

var result = (string)assembly.GetType("Server.TestContent.Driver")!
.GetMethod("Run")!
.Invoke(null, null)!;

Assert.Equal(
"null=0:True;addList=1;insert=2:4,5;addSet=3;addSetDuplicate=3;addDictionary=4;" +
"addDictionaryDuplicate=4:x;replace=5:null;comparer=6:1;removeMissing=6;remove=7;" +
"clearEmpty=7;clear=8:0;replaceNull=1:z",
result
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,26 @@ public static Assembly CompileAndLoad(
return Assembly.Load(stream.ToArray());
}

/// <summary>
/// Runs the generator in schema mode, as ModernUOSchemaGenerator does, and returns each migration
/// file it would write as (fileName, json), ready to feed back in as additional texts.
/// </summary>
public static List<(string fileName, string content)> GenerateMigrationSchemas(string assemblyName, string sourceCode)
{
var generator = new EntitySerializationGenerator(true);
RunGeneratorOnCompilation(assemblyName, sourceCode, null, generator);

var options = SerializableMigrationSchema.GetJsonSerializerOptions();
return generator.Migrations.Values
.Select(m => ($"{m.Type}.v{m.Version}.json", System.Text.Json.JsonSerializer.Serialize(m, options)))
.ToList();
}

private static (ImmutableArray<Diagnostic> Diagnostics, Compilation OutputCompilation) RunGeneratorOnCompilation(
string assemblyName,
string sourceCode,
IEnumerable<(string fileName, string content)>? additionalTexts)
IEnumerable<(string fileName, string content)>? additionalTexts,
EntitySerializationGenerator? generator = null)
{
var syntaxTrees = new List<SyntaxTree>
{
Expand All @@ -328,7 +344,7 @@ private static (ImmutableArray<Diagnostic> Diagnostics, Compilation OutputCompil
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);

var generator = new EntitySerializationGenerator();
generator ??= new EntitySerializationGenerator();

var additionalTextsList = new List<AdditionalText>();
if (additionalTexts != null)
Expand Down
144 changes: 135 additions & 9 deletions ModernUO.Serialization.Generator.Tests/MigrationSaveFlagTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class MigrationSaveFlagTests
{
// Binary reader/writer that sizes enums by their underlying type, like ModernUO's, so a width
// mismatch between writer and content struct misaligns the stream instead of going unnoticed.
private const string BinaryStreams = """
internal const string BinaryStreams = """
namespace Server.TestContent
{
using System;
Expand Down Expand Up @@ -44,10 +44,11 @@ public void Write(string value)
public void Write(float value) => _writer.Write(value);
public void Write(double value) => _writer.Write(value);
public void Write(decimal value) => _writer.Write(value);
public void Write(DateTime value) => throw new NotSupportedException();
public void WriteDeltaTime(DateTime value) => throw new NotSupportedException();
public void WriteAnchoredTime(DateTime value) => throw new NotSupportedException();
public void Write(TimeSpan value) => throw new NotSupportedException();
// Every time format is raw ticks here; only the stream alignment matters.
public void Write(DateTime value) => _writer.Write(value.Ticks);
public void WriteDeltaTime(DateTime value) => _writer.Write(value.Ticks);
public void WriteAnchoredTime(DateTime value) => _writer.Write(value.Ticks);
public void Write(TimeSpan value) => _writer.Write(value.Ticks);
public void Write(Guid value) => throw new NotSupportedException();
public void WriteEncodedInt(int value) => _writer.Write7BitEncodedInt(value);
public void Write<T>(T value) where T : struct, Enum => WriteEnum(value);
Expand Down Expand Up @@ -89,10 +90,10 @@ public sealed class BinaryGenericReader : Server.IGenericReader
public float ReadFloat() => _reader.ReadSingle();
public double ReadDouble() => _reader.ReadDouble();
public decimal ReadDecimal() => _reader.ReadDecimal();
public DateTime ReadDateTime() => throw new NotSupportedException();
public DateTime ReadDeltaTime() => throw new NotSupportedException();
public DateTime ReadAnchoredTime() => throw new NotSupportedException();
public TimeSpan ReadTimeSpan() => throw new NotSupportedException();
public DateTime ReadDateTime() => new(_reader.ReadInt64());
public DateTime ReadDeltaTime() => new(_reader.ReadInt64());
public DateTime ReadAnchoredTime() => new(_reader.ReadInt64());
public TimeSpan ReadTimeSpan() => new(_reader.ReadInt64());
public Guid ReadGuid() => throw new NotSupportedException();
public int ReadEncodedInt() => _reader.Read7BitEncodedInt();

Expand Down Expand Up @@ -143,6 +144,131 @@ public void MigrateFrom_ReadsEveryFieldWrittenByTheLiveGenerator(int flagCount)
Assert.Equal($"tag|{expected}", migrated);
}

// An absent timer flag must leave the content at the "no timer was running" sentinels
// (Next == DateTime.MinValue, Delay == TimeSpan.MinValue); a present one resumes its delay.
// Covers an anchored timer and a wall-clock one; the trailing flagged int proves alignment.
[Theory]
[InlineData(true)]
[InlineData(false)]
public void MigrateFrom_SeesTimerSentinelsWhenTheSaveFlagIsAbsent(bool running)
{
var bytes = WriteTimerV0(running);
var migrated = ReadTimerAsV1(running, bytes);

var timer = running ? "True,False,False" : "False,True,True";
Assert.Equal($"{timer}|{timer}|7", migrated);
}

private const string TimerEntityMembers = """
public FlaggedTimerItem() { }
public System.DateTime Created { get; set; }
public Server.Serial Serial { get; }
public bool Deleted => false;
public void Delete() { }
""";

private static byte[] WriteTimerV0(bool running)
{
var timerValue = running ? "new Server.Timer { Next = Server.Core.Now.AddHours(1) }" : "null";
var source = $$"""
using ModernUO.Serialization;
namespace Server.TestContent {
[SerializationGenerator(0)]
public partial class FlaggedTimerItem : Server.ISerializable {
[SerializableField(0)]
[SaveFlag(nameof(ShouldSerializeAnchored))]
[DeserializeTimer(nameof(RestartAnchored))]
private Server.Timer _anchoredTimer;
private bool ShouldSerializeAnchored() => _anchoredTimer != null;
private void RestartAnchored(System.TimeSpan delay) { }

[SerializableField(1)]
[SaveFlag(nameof(ShouldSerializeDeadline))]
[DeserializeTimer(nameof(RestartDeadline), wallClock: true)]
private Server.Timer _deadlineTimer;
private bool ShouldSerializeDeadline() => _deadlineTimer != null;
private void RestartDeadline(System.TimeSpan delay) { }

[SerializableField(2)]
[SaveFlag(nameof(ShouldSerializeCount))]
private int _count;
private bool ShouldSerializeCount() => _count != 0;

{{TimerEntityMembers}}

public static byte[] WriteSample() {
var item = new FlaggedTimerItem { _anchoredTimer = {{timerValue}}, _deadlineTimer = {{timerValue}}, _count = 7 };
var stream = new System.IO.MemoryStream();
var writer = new BinaryGenericWriter(stream);
item.Serialize(writer);
writer.Flush();
return stream.ToArray();
}
}
}
""";

var assembly = SourceGeneratorTestHelper.CompileAndLoad($"FlaggedTimerV0_{running}", source + BinaryStreams);

return (byte[])assembly.GetType("Server.TestContent.FlaggedTimerItem")!
.GetMethod("WriteSample")!
.Invoke(null, null)!;
}

private static string ReadTimerAsV1(bool running, byte[] bytes)
{
var source = $$"""
using System;
using ModernUO.Serialization;
namespace Server.TestContent {
[SerializationGenerator(1)]
public partial class FlaggedTimerItem : Server.ISerializable {
[SerializableField(0)] private string _migrated;

{{TimerEntityMembers}}

private static string Describe(DateTime next, TimeSpan delay) =>
$"{delay > TimeSpan.Zero},{next == DateTime.MinValue},{delay == TimeSpan.MinValue}";

private void MigrateFrom(V0Content content) {
_migrated = Describe(content.AnchoredTimerNext, content.AnchoredTimerDelay) + "|" +
Describe(content.DeadlineTimerNext, content.DeadlineTimerDelay) + "|" + content.Count;
}

public static string ReadSample(byte[] bytes) {
var reader = new BinaryGenericReader(new System.IO.MemoryStream(bytes));
var item = new FlaggedTimerItem();
item.Deserialize(reader);
if (!reader.AtEnd) throw new InvalidOperationException("Unread bytes remain");
return item._migrated;
}
}
}
""";

const string json = """
{
"version": 0,
"type": "Server.TestContent.FlaggedTimerItem",
"properties": [
{ "name": "AnchoredTimer", "type": "Server.Timer", "usesSaveFlag": true, "rule": "TimerMigrationRule", "ruleArguments": ["@AnchoredTimer"] },
{ "name": "DeadlineTimer", "type": "Server.Timer", "usesSaveFlag": true, "rule": "TimerMigrationRule", "ruleArguments": [""] },
{ "name": "Count", "type": "int", "usesSaveFlag": true, "rule": "PrimitiveTypeMigrationRule" }
]
}
""";

var assembly = SourceGeneratorTestHelper.CompileAndLoad(
$"FlaggedTimerV1_{running}",
source + BinaryStreams,
[("Server.TestContent.FlaggedTimerItem.v0.json", json)]
);

return (string)assembly.GetType("Server.TestContent.FlaggedTimerItem")!
.GetMethod("ReadSample")!
.Invoke(null, [bytes])!;
}

private static byte[] WriteV0(int flagCount)
{
var source = new StringBuilder();
Expand Down
Loading
Loading