Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix saving legacy summaries #423

Merged
merged 2 commits into from
Feb 24, 2025
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
339 changes: 339 additions & 0 deletions Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs

Large diffs are not rendered by default.

144 changes: 144 additions & 0 deletions Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using Xunit;

namespace Yafc.Model.Serialization.Tests;

public class SerializationTypeValidation {
[Fact]
// Ensure that all concrete types derived from ModelObject obey the serialization rules.
public void ModelObjects_AreSerializable() {
var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())
.Where(t => typeof(ModelObject).IsAssignableFrom(t) && !t.IsAbstract);

foreach (Type type in types) {
ConstructorInfo constructor = FindConstructor(type);

PropertyInfo ownerProperty = type.GetProperty("owner");
if (ownerProperty != null) {
// If derived from ModelObject<T> (as tested by "There's an 'owner' property"), the first constructor parameter must be T.
Assert.True(constructor.GetParameters().Length > 0, $"The first constructor parameter for type {MakeTypeName(type)} should be the parent object.");
Type baseType = typeof(ModelObject<>).MakeGenericType(ownerProperty.PropertyType);
Assert.True(baseType.IsAssignableFrom(type), $"The first constructor parameter for type {MakeTypeName(type)} is not the parent type.");
}

// Cheating a bit here: Project is the only ModelObject that is not a ModelObject<T>, and its constructor has no parameters.
// So we're just skipping a parameter that doesn't exist.
AssertConstructorParameters(type, constructor.GetParameters().Skip(1));
AssertSettableProperties(type);
AssertDictionaryKeys(type);
}
}

[Fact]
// Ensure that all [Serializable] types in the Yafc namespace obey the serialization rules,
// except compiler-generated types and those in Yafc.Blueprints.
public void Serializables_AreSerializable() {
var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())
.Where(t => t.GetCustomAttribute<SerializableAttribute>() != null && t.FullName.StartsWith("Yafc.") && !t.FullName.StartsWith("Yafc.Blueprints"));

foreach (Type type in types.Where(type => type.GetCustomAttribute<CompilerGeneratedAttribute>() == null)) {
ConstructorInfo constructor = FindConstructor(type);

AssertConstructorParameters(type, constructor.GetParameters());
AssertSettableProperties(type);
AssertDictionaryKeys(type);
}
}

internal static ConstructorInfo FindConstructor(Type type) {
BindingFlags flags = BindingFlags.Instance;
if (type.GetCustomAttribute<DeserializeWithNonPublicConstructorAttribute>() != null) {
flags |= BindingFlags.NonPublic;
}
else {
flags |= BindingFlags.Public;
}

// The constructor (public or non-public, depending on the attribute) must exist
ConstructorInfo constructor = type.GetConstructors(flags).FirstOrDefault();
Assert.True(constructor != null, $"Could not find the constructor for type {MakeTypeName(type)}.");
return constructor;
}


private static void AssertConstructorParameters(Type type, IEnumerable<ParameterInfo> parameters) {
foreach (ParameterInfo parameter in parameters) {
// Constructor parameters must be IsValueSerializerSupported
Assert.True(ValueSerializer.IsValueSerializerSupported(parameter.ParameterType),
$"Constructor of type {MakeTypeName(type)} parameter '{parameter.Name}' should be a supported value type.");

// and must have a matching property
PropertyInfo property = type.GetProperty(parameter.Name);
Assert.True(property != null, $"Constructor of type {MakeTypeName(type)} parameter '{parameter.Name}' does not have a matching property.");
Assert.True(parameter.ParameterType == property.PropertyType,
$"Constructor of type {MakeTypeName(type)} parameter '{parameter.Name}' does not have the same type as its property.");
}
}

private static void AssertSettableProperties(Type type) {
foreach (PropertyInfo property in type.GetProperties().Where(p => p.GetSetMethod() != null)) {
if (property.GetCustomAttribute<SkipSerializationAttribute>() == null) {
// Properties with a public setter must be IsValueSerializerSupported
Assert.True(ValueSerializer.IsValueSerializerSupported(property.PropertyType),
$"The type of property {MakeTypeName(type)}.{property.Name} should be a supported value type.");
}
}
}

private void AssertDictionaryKeys(Type type) {
foreach (PropertyInfo property in type.GetProperties().Where(p => p.GetSetMethod() == null)) {
if (property.GetCustomAttribute<SkipSerializationAttribute>() == null) {
Type iDictionary = property.PropertyType.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>));

if (iDictionary != null) {
if (ValueSerializer.IsValueSerializerSupported(iDictionary.GenericTypeArguments[0])
&& ValueSerializer.IsValueSerializerSupported(iDictionary.GenericTypeArguments[1])) {

object serializer = typeof(ValueSerializer<>).MakeGenericType(iDictionary.GenericTypeArguments[0]).GetField("Default").GetValue(null);
MethodInfo getJsonProperty = serializer.GetType().GetMethod(nameof(ValueSerializer<string>.GetJsonProperty));
// Dictionary keys must be serialized by an overridden ValueSerializer<T>.GetJsonProperty() method.
Assert.True(getJsonProperty != getJsonProperty.GetBaseDefinition(),
$"In {MakeTypeName(type)}.{property.Name}, the dictionary keys are of an unsupported type.");
}
}
}
}
}

internal static string MakeTypeName(Type type) {
if (type.IsGenericType) {
if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) {
return MakeTypeName(type.GetGenericArguments()[0]) + '?';
}
StringBuilder result = new(type.Name.Split('`')[0]);
result.Append('<').AppendJoin(", ", type.GenericTypeArguments.Select(MakeTypeName)).Append('>');
return result.ToString();
}

if (type == typeof(bool)) { return "bool"; }
if (type == typeof(byte)) { return "byte"; }
if (type == typeof(sbyte)) { return "sbyte"; }
if (type == typeof(char)) { return "char"; }
if (type == typeof(short)) { return "short"; }
if (type == typeof(ushort)) { return "ushort"; }
if (type == typeof(int)) { return "int"; }
if (type == typeof(uint)) { return "uint"; }
if (type == typeof(long)) { return "long"; }
if (type == typeof(ulong)) { return "ulong"; }
if (type == typeof(nint)) { return "nint"; }
if (type == typeof(nuint)) { return "nuint"; }
if (type == typeof(float)) { return "float"; }
if (type == typeof(double)) { return "double"; }
if (type == typeof(decimal)) { return "decimal"; }
if (type == typeof(string)) { return "string"; }
if (type == typeof(object)) { return "object"; }

return type.Name;
}
}
6 changes: 3 additions & 3 deletions Yafc.Model/Model/ProductionSummary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ public void SetMultiplier(float newMultiplier) {
}
}

public class ProductionSummaryColumn(ProductionSummary owner, IObjectWithQuality<Goods> goods) : ModelObject<ProductionSummary>(owner) {
public IObjectWithQuality<Goods> goods { get; } = goods ?? throw new ArgumentNullException(nameof(goods), "Object does not exist");
public class ProductionSummaryColumn(ProductionSummary owner, ObjectWithQuality<Goods> goods) : ModelObject<ProductionSummary>(owner) {
public ObjectWithQuality<Goods> goods { get; } = goods ?? throw new ArgumentNullException(nameof(goods), "Object does not exist");
}

public class ProductionSummary : ProjectPageContents, IComparer<(IObjectWithQuality<Goods> goods, float amount)> {
Expand All @@ -169,7 +169,7 @@ public class ProductionSummary : ProjectPageContents, IComparer<(IObjectWithQual
[SkipSerialization] public HashSet<IObjectWithQuality<Goods>> columnsExist { get; } = [];

public override void InitNew() {
columns.Add(new ProductionSummaryColumn(this, Database.electricity));
columns.Add(new ProductionSummaryColumn(this, new(Database.electricity.target, Quality.Normal)));
base.InitNew();
}

Expand Down
2 changes: 1 addition & 1 deletion Yafc.Model/Model/ProductionTableContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public class RecipeRowCustomModule(ModuleTemplate owner, ObjectWithQuality<Modul
/// The template that determines what modules are (or will be) applied to a <see cref="RecipeRow"/>.
/// </summary>
/// <remarks>Immutable. To modify, call <see cref="GetBuilder"/>, modify the builder, and call <see cref="ModuleTemplateBuilder.Build"/>.</remarks>
[Serializable, DeserializeWithNonPublicConstructor]
[DeserializeWithNonPublicConstructor]
public class ModuleTemplate : ModelObject<ModelObject> {
/// <summary>
/// The beacon to use, if any, for the associated <see cref="RecipeRow"/>.
Expand Down
4 changes: 1 addition & 3 deletions Yafc.Model/Yafc.Model.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Yafc.Model.Tests" />
<PackageReference Include="Google.OrTools" Version="9.11.4210" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Yafc.UI\Yafc.UI.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ private void AddOrRemoveColumn(IObjectWithQuality<Goods> goods) {
}

if (!found) {
model.columns.Add(new ProductionSummaryColumn(model, goods));
model.columns.Add(new ProductionSummaryColumn(model, new(goods.target, goods.quality)));
}
}

Expand Down
2 changes: 2 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Version:
Date:
Features:
- Add related recipes to the Link Summary screen.
Fixes:
- (regression) Legacy summary pages could not be saved or loaded.
----------------------------------------------------------------------------------------------------------------------
Version: 2.8.1
Date: February 20th 2025
Expand Down