Add tests for FieldLoader and FieldSaver

Test coverage for these classes will help prevent regressions. Major breaking changes are avoided since config files may already rely on various aspects of the behaviour, but some small breaking changes are made:

- Use `value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)` consistently for all comma-separated parsing.
- Rename LoadField to LoadFieldOrProperty for clarity - since this is the one method that cares about properties in a class otherwise focused only on fields.
- Use TryParse instead of Parse and ensure to call InvalidValueAction from all parsers so we throw the intended exception when badly formatted data is encountered.
- BooleanExpression/IntegerExpression exception message updated to align with the generic one.
- Remove FromYamlKey, and update the only user SkirmishLogic to perform this logic manually instead.
- Remove unused includePrivateByDefault parameter on GetTypeLoadInfo.
- Remove unused AllowEmptyEntriesAttribute.
This commit is contained in:
RoosterDragon
2025-11-15 12:03:39 +00:00
committed by Gustas Kažukauskas
parent 7d0340ad41
commit bc1a901f54
13 changed files with 1198 additions and 200 deletions

View File

@@ -0,0 +1,22 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
namespace OpenRA
{
// Mirrors DescriptionAttribute from System.ComponentModel but we don't want to have to use that everywhere.
[AttributeUsage(AttributeTargets.All)]
public sealed class DescAttribute(params string[] lines) : Attribute
{
public readonly string[] Lines = lines;
}
}

View File

@@ -548,7 +548,7 @@ namespace OpenRA
public static bool TryParseFloatOrPercentInvariant(string s, out float f)
{
if (float.TryParse(s.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out f))
if (float.TryParse(s?.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out f))
{
f *= s.Contains('%') ? 0.01f : 1f;
return true;

View File

@@ -23,7 +23,7 @@ namespace OpenRA
{
public static class FieldLoader
{
const char SplitComma = ',';
const char Comma = ',';
public class MissingFieldsException : YamlException
{
@@ -47,7 +47,7 @@ namespace OpenRA
}
public static Func<string, Type, string, object> InvalidValueAction = (s, t, f) =>
throw new YamlException($"FieldLoader: Cannot parse `{s}` into `{f}.{t}` ");
throw new YamlException($"FieldLoader: Cannot parse `{s}` into `{f}.{t}`");
public static Action<string, Type> UnknownFieldAction = (s, f) =>
throw new NotImplementedException($"FieldLoader: Missing field `{s}` on `{f.Name}`");
@@ -59,7 +59,7 @@ namespace OpenRA
static readonly ConcurrentCache<string, IntegerExpression> IntegerExpressionCache =
new(expression => new IntegerExpression(expression));
static readonly Dictionary<Type, Func<string, Type, string, MemberInfo, object>> TypeParsers =
static readonly Dictionary<Type, Func<string, Type, string, object>> TypeParsers =
new()
{
{ typeof(int), ParseInt },
@@ -83,7 +83,6 @@ namespace OpenRA
{ typeof(CVec[]), ParseCVecArray },
{ typeof(BooleanExpression), ParseBooleanExpression },
{ typeof(IntegerExpression), ParseIntegerExpression },
{ typeof(Enum), ParseEnum },
{ typeof(bool), ParseBool },
{ typeof(int2[]), ParseInt2Array },
{ typeof(Size), ParseSize },
@@ -94,7 +93,7 @@ namespace OpenRA
{ typeof(DateTime), ParseDateTime }
};
static readonly Dictionary<Type, Func<string, Type, string, MiniYaml, MemberInfo, object>> GenericTypeParsers =
static readonly Dictionary<Type, Func<string, Type, string, MiniYaml, object>> GenericTypeParsers =
new()
{
{ typeof(HashSet<>), ParseHashSetOrList },
@@ -108,7 +107,7 @@ namespace OpenRA
static readonly object BoxedFalse = false;
static readonly object[] BoxedInts = Exts.MakeArray(33, i => (object)i);
static object ParseInt(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseInt(string fieldName, Type fieldType, string value)
{
if (Exts.TryParseInt32Invariant(value, out var res))
{
@@ -120,48 +119,48 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseUShort(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseUShort(string fieldName, Type fieldType, string value)
{
if (ushort.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var res))
if (Exts.TryParseUshortInvariant(value, out var res))
return res;
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseLong(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseLong(string fieldName, Type fieldType, string value)
{
if (long.TryParse(value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out var res))
if (Exts.TryParseInt64Invariant(value, out var res))
return res;
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseFloat(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseFloat(string fieldName, Type fieldType, string value)
{
if (value != null && float.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
return res * (value.Contains('%') ? 0.01f : 1f);
if (Exts.TryParseFloatOrPercentInvariant(value, out var res))
return res;
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseDecimal(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseDecimal(string fieldName, Type fieldType, string value)
{
if (value != null && decimal.TryParse(value.Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
return res * (value.Contains('%') ? 0.01m : 1m);
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseString(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseString(string fieldName, Type fieldType, string value)
{
return value;
}
static object ParseColor(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseColor(string fieldName, Type fieldType, string value)
{
if (value != null && Color.TryParse(value, out var color))
if (Color.TryParse(value, out var color))
return color;
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseHotkey(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseHotkey(string fieldName, Type fieldType, string value)
{
if (Hotkey.TryParse(value, out var res))
return res;
@@ -169,12 +168,12 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseHotkeyReference(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseHotkeyReference(string fieldName, Type fieldType, string value)
{
return Game.ModData.Hotkeys[value];
}
static object ParseWDist(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseWDist(string fieldName, Type fieldType, string value)
{
if (WDist.TryParse(value, out var res))
return res;
@@ -182,11 +181,11 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseWVec(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseWVec(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 3
&& WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry)
@@ -197,11 +196,11 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseWVecArray(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseWVecArray(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length % 3 != 0)
return InvalidValueAction(value, fieldType, fieldName);
@@ -214,6 +213,8 @@ namespace OpenRA
&& WDist.TryParse(parts[3 * i + 1], out var ry)
&& WDist.TryParse(parts[3 * i + 2], out var rz))
vecs[i] = new WVec(rx, ry, rz);
else
return InvalidValueAction(value, fieldType, fieldName);
}
return vecs;
@@ -222,11 +223,11 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseWPos(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseWPos(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 3
&& WDist.TryParse(parts[0], out var rx)
&& WDist.TryParse(parts[1], out var ry)
@@ -237,18 +238,18 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseWAngle(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseWAngle(string fieldName, Type fieldType, string value)
{
if (Exts.TryParseInt32Invariant(value, out var res))
return new WAngle(res);
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseWRot(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseWRot(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 3
&& Exts.TryParseInt32Invariant(parts[0], out var rr)
&& Exts.TryParseInt32Invariant(parts[1], out var rp)
@@ -259,27 +260,31 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseCPos(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseCPos(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 3)
return new CPos(
Exts.ParseInt32Invariant(parts[0]),
Exts.ParseInt32Invariant(parts[1]),
Exts.ParseByteInvariant(parts[2]));
return new CPos(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 3
&& Exts.TryParseInt32Invariant(parts[0], out var x)
&& Exts.TryParseInt32Invariant(parts[1], out var y)
&& Exts.TryParseByteInvariant(parts[2], out var layer))
return new CPos(x, y, layer);
if (parts.Length == 2
&& Exts.TryParseInt32Invariant(parts[0], out x)
&& Exts.TryParseInt32Invariant(parts[1], out y))
return new CPos(x, y);
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseCPosArray(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseCPosArray(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName);
@@ -287,9 +292,11 @@ namespace OpenRA
var vecs = new CPos[parts.Length / 2];
for (var i = 0; i < vecs.Length; i++)
{
if (int.TryParse(parts[2 * i], out var rx)
&& int.TryParse(parts[2 * i + 1], out var ry))
if (Exts.TryParseInt32Invariant(parts[2 * i], out var rx)
&& Exts.TryParseInt32Invariant(parts[2 * i + 1], out var ry))
vecs[i] = new CPos(rx, ry);
else
return InvalidValueAction(value, fieldType, fieldName);
}
return vecs;
@@ -298,22 +305,25 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseCVec(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseCVec(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new CVec(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 2
&& Exts.TryParseInt32Invariant(parts[0], out var x)
&& Exts.TryParseInt32Invariant(parts[1], out var y))
return new CVec(x, y);
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseCVecArray(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseCVecArray(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName);
@@ -321,9 +331,11 @@ namespace OpenRA
var vecs = new CVec[parts.Length / 2];
for (var i = 0; i < vecs.Length; i++)
{
if (int.TryParse(parts[2 * i], out var rx)
&& int.TryParse(parts[2 * i + 1], out var ry))
if (Exts.TryParseInt32Invariant(parts[2 * i], out var rx)
&& Exts.TryParseInt32Invariant(parts[2 * i + 1], out var ry))
vecs[i] = new CVec(rx, ry);
else
return InvalidValueAction(value, fieldType, fieldName);
}
return vecs;
@@ -332,7 +344,7 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseBooleanExpression(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseBooleanExpression(string fieldName, Type fieldType, string value)
{
if (value != null)
{
@@ -342,14 +354,14 @@ namespace OpenRA
}
catch (InvalidDataException e)
{
throw new YamlException(e.Message);
throw new YamlException($"FieldLoader: Cannot parse `{value}` into `{fieldName}.{fieldType}`: {e.Message}");
}
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseIntegerExpression(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseIntegerExpression(string fieldName, Type fieldType, string value)
{
if (value != null)
{
@@ -359,44 +371,50 @@ namespace OpenRA
}
catch (InvalidDataException e)
{
throw new YamlException(e.Message);
throw new YamlException($"FieldLoader: Cannot parse `{value}` into `{fieldName}.{fieldType}`: {e.Message}");
}
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseEnum(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseEnum(string fieldName, Type fieldType, string value)
{
try
// Will allow numeric values that fit the underlying type of the enum, even if they aren't defined enumeration members.
if (Enum.TryParse(fieldType, value, true, out var enumValue))
{
return Enum.Parse(fieldType, value, true);
}
catch (ArgumentException)
{
return InvalidValueAction(value, fieldType, fieldName);
return enumValue;
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseBool(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseBool(string fieldName, Type fieldType, string value)
{
if (bool.TryParse(value.ToLowerInvariant(), out var result))
if (bool.TryParse(value, out var result))
return result ? BoxedTrue : BoxedFalse;
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseInt2Array(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseInt2Array(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length % 2 != 0)
return InvalidValueAction(value, fieldType, fieldName);
var ints = new int2[parts.Length / 2];
for (var i = 0; i < ints.Length; i++)
ints[i] = new int2(Exts.ParseInt32Invariant(parts[2 * i]), Exts.ParseInt32Invariant(parts[2 * i + 1]));
{
if (Exts.TryParseInt32Invariant(parts[2 * i], out var x)
&& Exts.TryParseInt32Invariant(parts[2 * i + 1], out var y))
ints[i] = new int2(x, y);
else
return InvalidValueAction(value, fieldType, fieldName);
}
return ints;
}
@@ -404,109 +422,112 @@ namespace OpenRA
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseSize(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseSize(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new Size(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 2
&& Exts.TryParseInt32Invariant(parts[0], out var width)
&& Exts.TryParseInt32Invariant(parts[1], out var height))
return new Size(width, height);
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseInt2(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseInt2(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2)
return InvalidValueAction(value, fieldType, fieldName);
return new int2(Exts.ParseInt32Invariant(parts[0]), Exts.ParseInt32Invariant(parts[1]));
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 2
&& Exts.TryParseInt32Invariant(parts[0], out var x)
&& Exts.TryParseInt32Invariant(parts[1], out var y))
return new int2(x, y);
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseFloat2(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseFloat2(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
float xx = 0;
float yy = 0;
if (float.TryParse(parts[0].Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var res))
xx = res * (parts[0].Contains('%') ? 0.01f : 1f);
if (float.TryParse(parts[1].Replace("%", ""), NumberStyles.Float, NumberFormatInfo.InvariantInfo, out res))
yy = res * (parts[1].Contains('%') ? 0.01f : 1f);
return new float2(xx, yy);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 2
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out var x)
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out var y))
return new float2(x, y);
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseFloat3(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseFloat3(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
float.TryParse(parts[0], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var x);
float.TryParse(parts[1], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out var y);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 3
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out var x)
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out var y)
&& Exts.TryParseFloatOrPercentInvariant(parts[2], out var z))
return new float3(x, y, z);
// z component is optional for compatibility with older float2 definitions
float z = 0;
if (parts.Length > 2)
float.TryParse(parts[2], NumberStyles.Float, NumberFormatInfo.InvariantInfo, out z);
return new float3(x, y, z);
if (parts.Length == 2
&& Exts.TryParseFloatOrPercentInvariant(parts[0], out x)
&& Exts.TryParseFloatOrPercentInvariant(parts[1], out y))
return new float3(x, y, 0);
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseRectangle(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseRectangle(string fieldName, Type fieldType, string value)
{
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
return new Rectangle(
Exts.ParseInt32Invariant(parts[0]),
Exts.ParseInt32Invariant(parts[1]),
Exts.ParseInt32Invariant(parts[2]),
Exts.ParseInt32Invariant(parts[3]));
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 4
&& Exts.TryParseInt32Invariant(parts[0], out var x)
&& Exts.TryParseInt32Invariant(parts[1], out var y)
&& Exts.TryParseInt32Invariant(parts[2], out var width)
&& Exts.TryParseInt32Invariant(parts[3], out var height))
return new Rectangle(x, y, width, height);
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseDateTime(string fieldName, Type fieldType, string value, MemberInfo field)
static object ParseDateTime(string fieldName, Type fieldType, string value)
{
if (DateTime.TryParseExact(value, "yyyy-MM-dd HH-mm-ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dt))
return dt;
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseHashSetOrList(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
static object ParseHashSetOrList(string fieldName, Type fieldType, string value, MiniYaml yaml)
{
if (value == null)
return Activator.CreateInstance(fieldType);
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var set = Activator.CreateInstance(fieldType, parts.Length);
var arguments = fieldType.GetGenericArguments();
var addMethod = fieldType.GetMethod(nameof(List<object>.Add), arguments);
var addArgs = new object[1];
for (var i = 0; i < parts.Length; i++)
{
addArgs[0] = GetValue(fieldName, arguments[0], parts[i].Trim(), field);
addArgs[0] = GetValue(fieldName, arguments[0], parts[i]);
addMethod.Invoke(set, addArgs);
}
return set;
}
static object ParseDictionary(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
static object ParseDictionary(string fieldName, Type fieldType, string value, MiniYaml yaml)
{
if (yaml == null)
return Activator.CreateInstance(fieldType);
@@ -517,33 +538,36 @@ namespace OpenRA
var addArgs = new object[2];
foreach (var node in yaml.Nodes)
{
addArgs[0] = GetValue(fieldName, arguments[0], node.Key, field);
addArgs[1] = GetValue(fieldName, arguments[1], node.Value, field);
addArgs[0] = GetValue(fieldName, arguments[0], node.Key);
addArgs[1] = GetValue(fieldName, arguments[1], node.Value);
addMethod.Invoke(dict, addArgs);
}
return dict;
}
static object ParseBitSet(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
static object ParseBitSet(string fieldName, Type fieldType, string value, MiniYaml yaml)
{
if (value != null)
{
var parts = value.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var ctor = fieldType.GetConstructor([typeof(string[])]);
return ctor.Invoke([parts.Select(p => p.Trim()).ToArray()]);
return ctor.Invoke([parts]);
}
else
{
var ctor = fieldType.GetConstructor([typeof(string[])]);
return ctor.Invoke([Array.Empty<string>()]);
}
return InvalidValueAction(value, fieldType, fieldName);
}
static object ParseNullable(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
static object ParseNullable(string fieldName, Type fieldType, string value, MiniYaml yaml)
{
if (string.IsNullOrEmpty(value))
return null;
var innerType = fieldType.GetGenericArguments()[0];
var innerValue = GetValue("Nullable<T>", innerType, value, field);
var innerValue = GetValue("Nullable<T>", innerType, value);
return fieldType.GetConstructor([innerType]).Invoke([innerValue]);
}
@@ -598,7 +622,7 @@ namespace OpenRA
if (!md.TryGetValue(yamlName, out var yaml))
return false;
ret = GetValue(field.Name, field.FieldType, yaml, field);
ret = GetValue(field.Name, field.FieldType, yaml);
return true;
}
@@ -609,7 +633,7 @@ namespace OpenRA
return t;
}
public static void LoadField(object target, string key, string value)
public static void LoadFieldOrProperty(object target, string key, string value)
{
const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
@@ -618,18 +642,14 @@ namespace OpenRA
var field = target.GetType().GetField(key, Flags);
if (field != null)
{
var sa = field.GetCustomAttributes<SerializeAttribute>(false).DefaultIfEmpty(SerializeAttribute.Default).First();
if (!sa.FromYamlKey)
field.SetValue(target, GetValue(field.Name, field.FieldType, value, field));
field.SetValue(target, GetValue(field.Name, field.FieldType, value));
return;
}
var prop = target.GetType().GetProperty(key, Flags);
if (prop != null)
{
var sa = prop.GetCustomAttributes<SerializeAttribute>(false).DefaultIfEmpty(SerializeAttribute.Default).First();
if (!sa.FromYamlKey)
prop.SetValue(target, GetValue(prop.Name, prop.PropertyType, value, prop), null);
prop.SetValue(target, GetValue(prop.Name, prop.PropertyType, value), null);
return;
}
@@ -641,48 +661,44 @@ namespace OpenRA
return (T)GetValue(field, typeof(T), value, null);
}
public static object GetValue(string fieldName, Type fieldType, string value)
static object GetValue(string fieldName, Type fieldType, string value)
{
return GetValue(fieldName, fieldType, value, null);
}
public static object GetValue(string fieldName, Type fieldType, string value, MemberInfo field)
static object GetValue(string fieldName, Type fieldType, MiniYaml yaml)
{
return GetValue(fieldName, fieldType, value, null, field);
return GetValue(fieldName, fieldType, yaml.Value, yaml);
}
public static object GetValue(string fieldName, Type fieldType, MiniYaml yaml, MemberInfo field)
{
return GetValue(fieldName, fieldType, yaml.Value, yaml, field);
}
static object GetValue(string fieldName, Type fieldType, string value, MiniYaml yaml, MemberInfo field)
static object GetValue(string fieldName, Type fieldType, string value, MiniYaml yaml)
{
value = value?.Trim();
if (fieldType.IsGenericType)
{
if (GenericTypeParsers.TryGetValue(fieldType.GetGenericTypeDefinition(), out var parseFuncGeneric))
return parseFuncGeneric(fieldName, fieldType, value, yaml, field);
return parseFuncGeneric(fieldName, fieldType, value, yaml);
}
else
{
if (TypeParsers.TryGetValue(fieldType, out var parseFunc))
return parseFunc(fieldName, fieldType, value, field);
return parseFunc(fieldName, fieldType, value);
if (fieldType.IsArray && fieldType.GetArrayRank() == 1)
{
if (value == null)
return Array.CreateInstance(fieldType.GetElementType(), 0);
var options = field != null && field.HasAttribute<AllowEmptyEntriesAttribute>() ?
StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries;
var parts = value.Split(SplitComma, options);
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var ret = Array.CreateInstance(fieldType.GetElementType(), parts.Length);
for (var i = 0; i < parts.Length; i++)
ret.SetValue(GetValue(fieldName, fieldType.GetElementType(), parts[i].Trim(), field), i);
ret.SetValue(GetValue(fieldName, fieldType.GetElementType(), parts[i]), i);
return ret;
}
if (fieldType.IsEnum)
return ParseEnum(fieldName, fieldType, value);
}
var conv = TypeDescriptor.GetConverter(fieldType);
@@ -706,21 +722,20 @@ namespace OpenRA
{
public readonly FieldInfo Field;
public readonly SerializeAttribute Attribute;
public readonly string YamlName;
public readonly Func<MiniYaml, object> Loader;
public string YamlName => Field.Name;
internal FieldLoadInfo(FieldInfo field, SerializeAttribute attr, string yamlName, Func<MiniYaml, object> loader = null)
public FieldLoadInfo(FieldInfo field, SerializeAttribute attr, Func<MiniYaml, object> loader = null)
{
Field = field;
Attribute = attr;
YamlName = yamlName;
Loader = loader;
}
}
public static IEnumerable<FieldLoadInfo> GetTypeLoadInfo(Type type, bool includePrivateByDefault = false)
public static IEnumerable<FieldLoadInfo> GetTypeLoadInfo(Type type)
{
return TypeLoadInfo[type].Where(fli => includePrivateByDefault || fli.Field.IsPublic || (fli.Attribute.Serialize && !fli.Attribute.IsDefault));
return TypeLoadInfo[type].Where(fli => fli.Field.IsPublic || (fli.Attribute.Serialize && !fli.Attribute.IsDefault));
}
static FieldLoadInfo[] BuildTypeLoadInfo(Type type)
@@ -735,13 +750,9 @@ namespace OpenRA
if (!sa.Serialize)
continue;
var yamlName = string.IsNullOrEmpty(sa.YamlName) ? field.Name : sa.YamlName;
var loader = sa.GetLoader(type);
if (loader == null && sa.FromYamlKey)
loader = yaml => GetValue(yamlName, field.FieldType, yaml, field);
var fli = new FieldLoadInfo(field, sa, yamlName, loader);
var fli = new FieldLoadInfo(field, sa, loader);
ret.Add(fli);
}
@@ -752,31 +763,21 @@ namespace OpenRA
public sealed class IgnoreAttribute : SerializeAttribute
{
public IgnoreAttribute()
: base(false) { }
: base(serialize: false) { }
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class RequireAttribute : SerializeAttribute
{
public RequireAttribute()
: base(true, true) { }
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class AllowEmptyEntriesAttribute : SerializeAttribute
{
public AllowEmptyEntriesAttribute()
: base(allowEmptyEntries: true) { }
: base(serialize: true, required: true) { }
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class LoadUsingAttribute : SerializeAttribute
{
public LoadUsingAttribute(string loader, bool required = false)
{
Loader = loader;
Required = required;
}
: base(serialize: true, required, loader) { }
}
[AttributeUsage(AttributeTargets.Field)]
@@ -787,17 +788,14 @@ namespace OpenRA
public bool IsDefault => this == Default;
public readonly bool Serialize;
public string YamlName;
public string Loader;
public bool FromYamlKey;
public bool Required;
public bool AllowEmptyEntries;
public readonly bool Required;
public readonly string Loader;
public SerializeAttribute(bool serialize = true, bool required = false, bool allowEmptyEntries = false)
protected SerializeAttribute(bool serialize = true, bool required = false, string loader = null)
{
Serialize = serialize;
Required = required;
AllowEmptyEntries = allowEmptyEntries;
Loader = loader;
}
internal Func<MiniYaml, object> GetLoader(Type type)
@@ -817,20 +815,4 @@ namespace OpenRA
}
}
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class FieldFromYamlKeyAttribute : FieldLoader.SerializeAttribute
{
public FieldFromYamlKeyAttribute()
{
FromYamlKey = true;
}
}
// Mirrors DescriptionAttribute from System.ComponentModel but we don't want to have to use that everywhere.
[AttributeUsage(AttributeTargets.All)]
public sealed class DescAttribute(params string[] lines) : Attribute
{
public readonly string[] Lines = lines;
}
}

View File

@@ -22,12 +22,11 @@ namespace OpenRA
{
public static class FieldSaver
{
public static MiniYaml Save(object o, bool includePrivateByDefault = false)
public static MiniYaml Save(object o)
{
var nodes = new List<MiniYamlNode>();
string root = null;
foreach (var fieldInfo in FieldLoader.GetTypeLoadInfo(o.GetType(), includePrivateByDefault))
foreach (var fieldInfo in FieldLoader.GetTypeLoadInfo(o.GetType()))
{
if (fieldInfo.Field.FieldType.IsGenericType && fieldInfo.Field.FieldType.IsAssignableTo(typeof(System.Collections.IDictionary)))
{
@@ -42,21 +41,19 @@ namespace OpenRA
nodes.Add(new MiniYamlNode(fieldInfo.YamlName, "", dictNodes));
}
else if (fieldInfo.Attribute.FromYamlKey)
root = FormatValue(o, fieldInfo.Field);
else
nodes.Add(new MiniYamlNode(fieldInfo.YamlName, FormatValue(o, fieldInfo.Field)));
}
return new MiniYaml(root, nodes);
return new MiniYaml(null, nodes);
}
public static MiniYaml SaveDifferences(object o, object from, bool includePrivateByDefault = false)
public static MiniYaml SaveDifferences(object o, object from)
{
if (o.GetType() != from.GetType())
throw new InvalidOperationException("FieldLoader: can't diff objects of different types");
throw new InvalidOperationException("FieldSaver: can't diff objects of different types");
var fields = FieldLoader.GetTypeLoadInfo(o.GetType(), includePrivateByDefault)
var fields = FieldLoader.GetTypeLoadInfo(o.GetType())
.Where(info => FormatValue(o, info.Field) != FormatValue(from, info.Field));
return new MiniYaml(

View File

@@ -111,7 +111,7 @@ namespace OpenRA
else if (type == Type.MiniYaml)
field.SetValue(map, node.Value);
else
FieldLoader.LoadField(map, fieldName, node.Value.Value);
FieldLoader.LoadFieldOrProperty(map, fieldName, node.Value.Value);
}
if (property != null)
@@ -121,7 +121,7 @@ namespace OpenRA
else if (type == Type.MiniYaml)
property.SetValue(map, node.Value, null);
else
FieldLoader.LoadField(map, fieldName, node.Value.Value);
FieldLoader.LoadFieldOrProperty(map, fieldName, node.Value.Value);
}
}

View File

@@ -158,6 +158,9 @@ namespace OpenRA.Primitives
public static bool TryParse(string value, out Color color)
{
color = default;
if (value == null)
return false;
value = value.Trim();
if (value.Length != 6 && value.Length != 8)
return false;

View File

@@ -369,7 +369,7 @@ namespace OpenRA
foreach (var kv in Sections)
foreach (var f in kv.Value.GetType().GetFields())
if (args.Contains(kv.Key + "." + f.Name))
FieldLoader.LoadField(kv.Value, f.Name, args.GetValue(kv.Key + "." + f.Name, ""));
FieldLoader.LoadFieldOrProperty(kv.Value, f.Name, args.GetValue(kv.Key + "." + f.Name, ""));
}
finally
{

View File

@@ -38,7 +38,7 @@ namespace OpenRA
foreach (var f in GetType().GetFields())
if (args.Contains("Launch." + f.Name))
FieldLoader.LoadField(this, f.Name, args.GetValue("Launch." + f.Name, ""));
FieldLoader.LoadFieldOrProperty(this, f.Name, args.GetValue("Launch." + f.Name, ""));
}
public ConnectionTarget GetConnectEndPoint()

View File

@@ -581,6 +581,8 @@ namespace OpenRA.Support
Expression = expression;
}
public override string ToString() => Expression;
Expression Build(ExpressionType resultType)
{
var tokens = new List<Token>();

View File

@@ -55,11 +55,11 @@ namespace OpenRA
parent?.AddChild(widget);
if (node.Key.Contains('@'))
FieldLoader.LoadField(widget, "Id", node.Key.Split('@')[1]);
FieldLoader.LoadFieldOrProperty(widget, "Id", node.Key.Split('@')[1]);
foreach (var child in node.Value.Nodes)
if (child.Key != "Children")
FieldLoader.LoadField(widget, child.Key, child.Value.Value);
FieldLoader.LoadFieldOrProperty(widget, child.Key, child.Value.Value);
widget.Initialize(args);

View File

@@ -24,7 +24,9 @@ namespace OpenRA.Mods.Common.Server
{
sealed class SkirmishSlot
{
[FieldLoader.Serialize(FromYamlKey = true)]
static string LoadSlot(MiniYaml yaml) => yaml.Value;
[FieldLoader.LoadUsing(nameof(LoadSlot))]
public readonly string Slot;
public readonly Color Color;
public readonly string Faction;
@@ -54,6 +56,12 @@ namespace OpenRA.Mods.Common.Server
c.Team = s.Team;
c.Handicap = s.Handicap;
}
public MiniYaml ToYaml()
{
var yaml = FieldSaver.Save(this);
return yaml.WithValue(Slot).WithNodes(yaml.Nodes.RemoveAll(n => n.Key == nameof(Slot)));
}
}
static bool TryInitializeFromFile(S server, string path, Connection conn)
@@ -156,9 +164,9 @@ namespace OpenRA.Mods.Common.Server
new("Map", server.LobbyInfo.GlobalSettings.Map),
new("Options", new MiniYaml("", server.LobbyInfo.GlobalSettings.LobbyOptions
.Select(kv => new MiniYamlNode(kv.Key, kv.Value.Value)))),
new("Player", FieldSaver.Save(new SkirmishSlot(playerClient))),
new("Player", new SkirmishSlot(playerClient).ToYaml()),
new("Bots", new MiniYaml("", server.LobbyInfo.Clients.Where(c => c.IsBot)
.Select(b => new MiniYamlNode(b.Bot, FieldSaver.Save(new SkirmishSlot(b))))))
.Select(b => new MiniYamlNode(b.Bot, new SkirmishSlot(b).ToYaml()))))
}.WriteToFile(path);
}

View File

@@ -0,0 +1,719 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using NUnit.Framework.Internal;
using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA.Test
{
[TestFixture]
sealed class FieldLoaderTest
{
sealed class TypeInfo
{
#pragma warning disable CS0169
#pragma warning disable CS0649
#pragma warning disable CA1823 // Avoid unused private fields
#pragma warning disable IDE0044 // Add readonly modifier
#pragma warning disable IDE0051 // Remove unused private members
#pragma warning disable RCS1170 // Use read-only auto-implemented property
int privateField;
public int PublicField;
[FieldLoader.Ignore]
int privateIgnoreField;
[FieldLoader.Ignore]
public int PublicIgnoreField;
[FieldLoader.Require]
int privateRequiredField;
[FieldLoader.Require]
public int PublicRequiredField;
[FieldLoader.LoadUsing(nameof(LoadInt32))]
int privateLoadUsingField;
[FieldLoader.LoadUsing(nameof(LoadInt32))]
public int PublicLoadUsingField;
int PrivateProperty { get; set; }
public int PublicProperty { get; set; }
static int privateStaticField;
public static int PublicStaticField;
static object LoadInt32(MiniYaml _) => 123;
#pragma warning restore RCS1170 // Use read-only auto-implemented property
#pragma warning restore IDE0051 // Remove unused private members
#pragma warning restore IDE0044 // Add readonly modifier
#pragma warning restore CA1823 // Avoid unused private fields
#pragma warning disable CS0649
#pragma warning restore CS0169
}
[Test]
public void GetTypeLoadInfo()
{
var infos = FieldLoader.GetTypeLoadInfo(typeof(TypeInfo)).ToList();
Assert.That(infos, Has.Count.EqualTo(5));
Assert.That(infos[0].Field, Is.EqualTo(typeof(TypeInfo).GetField(nameof(TypeInfo.PublicField))));
Assert.That(infos[0].Attribute, Is.EqualTo(FieldLoader.SerializeAttribute.Default));
Assert.That(infos[0].YamlName, Is.EqualTo(nameof(TypeInfo.PublicField)));
Assert.That(infos[0].Loader, Is.Null);
Assert.That(infos[1].Field, Is.EqualTo(typeof(TypeInfo).GetField("privateRequiredField", BindingFlags.NonPublic | BindingFlags.Instance)));
Assert.That(infos[1].Attribute, Is.EqualTo(new FieldLoader.RequireAttribute()));
Assert.That(infos[1].YamlName, Is.EqualTo("privateRequiredField"));
Assert.That(infos[1].Loader, Is.Null);
Assert.That(infos[2].Field, Is.EqualTo(typeof(TypeInfo).GetField(nameof(TypeInfo.PublicRequiredField))));
Assert.That(infos[2].Attribute, Is.EqualTo(new FieldLoader.RequireAttribute()));
Assert.That(infos[2].YamlName, Is.EqualTo(nameof(TypeInfo.PublicRequiredField)));
Assert.That(infos[2].Loader, Is.Null);
Assert.That(infos[3].Field, Is.EqualTo(typeof(TypeInfo).GetField("privateLoadUsingField", BindingFlags.NonPublic | BindingFlags.Instance)));
Assert.That(infos[3].Attribute, Is.EqualTo(new FieldLoader.LoadUsingAttribute("LoadInt32")));
Assert.That(infos[3].YamlName, Is.EqualTo("privateLoadUsingField"));
Assert.That(infos[3].Loader(new MiniYaml(null)), Is.EqualTo(123));
Assert.That(infos[4].Field, Is.EqualTo(typeof(TypeInfo).GetField(nameof(TypeInfo.PublicLoadUsingField))));
Assert.That(infos[4].Attribute, Is.EqualTo(new FieldLoader.LoadUsingAttribute("LoadInt32")));
Assert.That(infos[4].YamlName, Is.EqualTo(nameof(TypeInfo.PublicLoadUsingField)));
Assert.That(infos[4].Loader(new MiniYaml(null)), Is.EqualTo(123));
}
[Test]
public void GetValue_UnknownField()
{
static void Act() => FieldLoader.GetValue<object>("field", "test");
Assert.That(Act, Throws.TypeOf<NotImplementedException>().And.Message.EqualTo("FieldLoader: Missing field `[Type] test` on `Object`"));
}
static IEnumerable<TestCaseData> GetValue_InvalidValue_TestCases()
{
return
[
new TestCaseData(null) { TypeArgs = [typeof(int)] },
new TestCaseData("test") { TypeArgs = [typeof(int)] },
new TestCaseData("1.2") { TypeArgs = [typeof(int)] },
new TestCaseData((int.MaxValue + 1L).ToString(CultureInfo.InvariantCulture)) { TypeArgs = [typeof(int)] },
new TestCaseData(null) { TypeArgs = [typeof(ushort)] },
new TestCaseData("test") { TypeArgs = [typeof(ushort)] },
new TestCaseData("1.2") { TypeArgs = [typeof(ushort)] },
new TestCaseData((ushort.MaxValue + 1L).ToString(CultureInfo.InvariantCulture)) { TypeArgs = [typeof(ushort)] },
new TestCaseData(null) { TypeArgs = [typeof(long)] },
new TestCaseData("test") { TypeArgs = [typeof(long)] },
new TestCaseData("1.2") { TypeArgs = [typeof(long)] },
new TestCaseData((long.MaxValue + 1UL).ToString(CultureInfo.InvariantCulture)) { TypeArgs = [typeof(long)] },
new TestCaseData(null) { TypeArgs = [typeof(float)] },
new TestCaseData("test") { TypeArgs = [typeof(float)] },
new TestCaseData("1,2") { TypeArgs = [typeof(float)] },
new TestCaseData(null) { TypeArgs = [typeof(decimal)] },
new TestCaseData("test") { TypeArgs = [typeof(decimal)] },
new TestCaseData("1,2") { TypeArgs = [typeof(decimal)] },
new TestCaseData(null) { TypeArgs = [typeof(Color)] },
new TestCaseData("test") { TypeArgs = [typeof(Color)] },
new TestCaseData(null) { TypeArgs = [typeof(Hotkey)] },
new TestCaseData("test") { TypeArgs = [typeof(Hotkey)] },
new TestCaseData(null) { TypeArgs = [typeof(WDist)] },
new TestCaseData("test") { TypeArgs = [typeof(WDist)] },
new TestCaseData(null) { TypeArgs = [typeof(WVec)] },
new TestCaseData("test") { TypeArgs = [typeof(WVec)] },
new TestCaseData("1,2") { TypeArgs = [typeof(WVec)] },
new TestCaseData("1,test,3") { TypeArgs = [typeof(WVec)] },
new TestCaseData("1,2,3,4") { TypeArgs = [typeof(WVec)] },
new TestCaseData(null) { TypeArgs = [typeof(WVec[])] },
new TestCaseData("test") { TypeArgs = [typeof(WVec[])] },
new TestCaseData("1,2") { TypeArgs = [typeof(WVec[])] },
new TestCaseData("1,test,3") { TypeArgs = [typeof(WVec[])] },
new TestCaseData("1,2,3,4") { TypeArgs = [typeof(WVec[])] },
new TestCaseData(null) { TypeArgs = [typeof(WPos)] },
new TestCaseData("test") { TypeArgs = [typeof(WPos)] },
new TestCaseData("1,2") { TypeArgs = [typeof(WPos)] },
new TestCaseData("1,test,3") { TypeArgs = [typeof(WPos)] },
new TestCaseData("1,2,3,4") { TypeArgs = [typeof(WPos)] },
new TestCaseData(null) { TypeArgs = [typeof(WAngle)] },
new TestCaseData("test") { TypeArgs = [typeof(WAngle)] },
new TestCaseData("1,2") { TypeArgs = [typeof(WAngle)] },
new TestCaseData(null) { TypeArgs = [typeof(WRot)] },
new TestCaseData("test") { TypeArgs = [typeof(WRot)] },
new TestCaseData("1,2") { TypeArgs = [typeof(WRot)] },
new TestCaseData("1,test,3") { TypeArgs = [typeof(WRot)] },
new TestCaseData("1,2,3,4") { TypeArgs = [typeof(WRot)] },
new TestCaseData(null) { TypeArgs = [typeof(CPos)] },
new TestCaseData("test") { TypeArgs = [typeof(CPos)] },
new TestCaseData("1") { TypeArgs = [typeof(CPos)] },
new TestCaseData("1,test,3") { TypeArgs = [typeof(CPos)] },
new TestCaseData("1,2,3,4") { TypeArgs = [typeof(CPos)] },
new TestCaseData(null) { TypeArgs = [typeof(CPos[])] },
new TestCaseData("test") { TypeArgs = [typeof(CPos[])] },
new TestCaseData("1") { TypeArgs = [typeof(CPos[])] },
new TestCaseData("1,test") { TypeArgs = [typeof(CPos[])] },
new TestCaseData("1,2,3") { TypeArgs = [typeof(CPos[])] },
new TestCaseData(null) { TypeArgs = [typeof(CVec)] },
new TestCaseData("test") { TypeArgs = [typeof(CVec)] },
new TestCaseData("1") { TypeArgs = [typeof(CVec)] },
new TestCaseData("1,test") { TypeArgs = [typeof(CVec)] },
new TestCaseData("1,2,3") { TypeArgs = [typeof(CVec)] },
new TestCaseData(null) { TypeArgs = [typeof(CVec[])] },
new TestCaseData("test") { TypeArgs = [typeof(CVec[])] },
new TestCaseData("1") { TypeArgs = [typeof(CVec[])] },
new TestCaseData("1,test") { TypeArgs = [typeof(CVec[])] },
new TestCaseData("1,2,3") { TypeArgs = [typeof(CVec[])] },
new TestCaseData(null) { TypeArgs = [typeof(BooleanExpression)] },
new TestCaseData(null) { TypeArgs = [typeof(IntegerExpression)] },
new TestCaseData(null) { TypeArgs = [typeof(MapGridType)] },
new TestCaseData(null) { TypeArgs = [typeof(bool)] },
new TestCaseData("test") { TypeArgs = [typeof(bool)] },
new TestCaseData(null) { TypeArgs = [typeof(int2[])] },
new TestCaseData("test") { TypeArgs = [typeof(int2[])] },
new TestCaseData("1") { TypeArgs = [typeof(int2[])] },
new TestCaseData("1,test") { TypeArgs = [typeof(int2[])] },
new TestCaseData("1,2,3") { TypeArgs = [typeof(int2[])] },
new TestCaseData(null) { TypeArgs = [typeof(Size)] },
new TestCaseData("test") { TypeArgs = [typeof(Size)] },
new TestCaseData("1") { TypeArgs = [typeof(Size)] },
new TestCaseData("1,test") { TypeArgs = [typeof(Size)] },
new TestCaseData("1,2,3") { TypeArgs = [typeof(Size)] },
new TestCaseData(null) { TypeArgs = [typeof(int2)] },
new TestCaseData("test") { TypeArgs = [typeof(int2)] },
new TestCaseData("1") { TypeArgs = [typeof(int2)] },
new TestCaseData("1,test") { TypeArgs = [typeof(int2)] },
new TestCaseData("1,2,3") { TypeArgs = [typeof(int2)] },
new TestCaseData(null) { TypeArgs = [typeof(float2)] },
new TestCaseData("test") { TypeArgs = [typeof(float2)] },
new TestCaseData("1") { TypeArgs = [typeof(float2)] },
new TestCaseData("1,test") { TypeArgs = [typeof(float2)] },
new TestCaseData("1,2,3") { TypeArgs = [typeof(float2)] },
new TestCaseData(null) { TypeArgs = [typeof(float3)] },
new TestCaseData("test") { TypeArgs = [typeof(float3)] },
new TestCaseData("1") { TypeArgs = [typeof(float3)] },
new TestCaseData("1,test") { TypeArgs = [typeof(float3)] },
new TestCaseData("1,2,3,4") { TypeArgs = [typeof(float3)] },
new TestCaseData(null) { TypeArgs = [typeof(Rectangle)] },
new TestCaseData("test") { TypeArgs = [typeof(Rectangle)] },
new TestCaseData("1,2,3") { TypeArgs = [typeof(Rectangle)] },
new TestCaseData("1,test,3,4") { TypeArgs = [typeof(Rectangle)] },
new TestCaseData("1,2,3,4,5") { TypeArgs = [typeof(Rectangle)] },
new TestCaseData(null) { TypeArgs = [typeof(DateTime)] },
new TestCaseData("test") { TypeArgs = [typeof(DateTime)] },
new TestCaseData("2000-01-01") { TypeArgs = [typeof(DateTime)] },
];
}
[TestCaseSource(nameof(GetValue_InvalidValue_TestCases))]
public void GetValue_InvalidValue<T>(string input)
{
void Act() => FieldLoader.GetValue<T>("field", input);
Assert.That(Act, Throws.TypeOf<YamlException>().And.Message.EqualTo($"FieldLoader: Cannot parse `{input}` into `field.{typeof(T).FullName}`"));
}
[TestCase(TypeArgs = [typeof(BooleanExpression)])]
[TestCase(TypeArgs = [typeof(IntegerExpression)])]
public void GetValue_InvalidValue<T>()
{
static void Act() => FieldLoader.GetValue<T>("field", "");
Assert.That(Act, Throws.TypeOf<YamlException>().And.Message.EqualTo($"FieldLoader: Cannot parse `` into `field.{typeof(T).FullName}`: Empty expression"));
}
static IEnumerable<TestCaseData> GetValue_Primitive_TestCases()
{
return
[
new TestCaseData(123),
new TestCaseData((ushort)123),
new TestCaseData(123L),
new TestCaseData(123.4f),
new TestCaseData(123m),
new TestCaseData("test"),
new TestCaseData(Color.CornflowerBlue),
new TestCaseData(new Hotkey(Keycode.A, Modifiers.Shift)),
new TestCaseData(new WDist(123)),
new TestCaseData(new WVec(123, 456, 789)),
new TestCaseData(new WPos(123, 456, 789)),
new TestCaseData(new WAngle(123)),
new TestCaseData(new WRot(new WAngle(123), new WAngle(456), new WAngle(789))),
new TestCaseData(new CPos(123, 456)),
new TestCaseData(new CPos(123, 456, 78)),
new TestCaseData(new CVec(123, 456)),
new TestCaseData(MapGridType.RectangularIsometric),
new TestCaseData((MapGridType)byte.MaxValue),
new TestCaseData(SystemActors.World | SystemActors.EditorWorld),
new TestCaseData(true),
new TestCaseData(new Size(123, 456)),
new TestCaseData(new int2(123, 456)),
new TestCaseData(new float2(123, 456)),
new TestCaseData(new float3(123, 456, 789)),
new TestCaseData(new Rectangle(123, 456, 789, 123)),
];
}
[TestCaseSource(nameof(GetValue_Primitive_TestCases))]
public void GetValue_Primitive<T>(T expected)
{
var actual = FieldLoader.GetValue<T>("field", $" {expected} ");
Assert.That(actual, Is.EqualTo(expected));
}
[Test]
public void GetValue_BooleanExpression()
{
var actual = FieldLoader.GetValue<BooleanExpression>("field", " true ");
Assert.That(actual.Expression, Is.EqualTo("true"));
}
[Test]
public void GetValue_IntegerExpression()
{
var actual = FieldLoader.GetValue<IntegerExpression>("field", " 1 + 2 ");
Assert.That(actual.Expression, Is.EqualTo("1 + 2"));
}
[Test]
public void GetValue_DateTime()
{
var expected = new DateTime(2000, 1, 1);
var input = expected.ToString("yyyy-MM-dd HH-mm-ss", CultureInfo.InvariantCulture);
var actual = FieldLoader.GetValue<DateTime>("field", $" {input} ");
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("123%", 1.23f)]
[TestCase("%123", 1.23f)]
[TestCase("123.456%", 1.23456f)]
[TestCase("%123.456", 1.23456f)]
public void GetValue_FloatPercentage(string input, float expected)
{
var actual = FieldLoader.GetValue<float>("field", $" {input} ");
Assert.That(actual, Is.EqualTo(expected));
}
static IEnumerable<TestCaseData> GetValue_DecimalPercentage_TestCases()
{
return
[
new TestCaseData("123%", 1.23m),
new TestCaseData("%123", 1.23m),
new TestCaseData("123.456%", 1.23456m),
new TestCaseData("%123.456", 1.23456m),
];
}
[TestCaseSource(nameof(GetValue_DecimalPercentage_TestCases))]
public void GetValue_DecimalPercentage(string input, decimal expected)
{
var actual = FieldLoader.GetValue<decimal>("field", $" {input} ");
Assert.That(actual, Is.EqualTo(expected));
}
[Test]
public void GetValue_float3_TwoElements()
{
var actual = FieldLoader.GetValue<float3>("field", "123,456");
Assert.That(actual, Is.EqualTo(new float3(123, 456, 0)));
}
[Test]
public void GetValue_WVecArray()
{
var actual = FieldLoader.GetValue<WVec[]>("field", " 1 , 2 , 3 , 4 , 5 , 6 , , ");
Assert.That(actual, Is.EqualTo(new WVec[] { new(1, 2, 3), new(4, 5, 6) }));
}
[Test]
public void GetValue_CPosArray()
{
var actual = FieldLoader.GetValue<CPos[]>("field", " 1 , 2 , 3 , 4 , 5 , 6 , , ");
Assert.That(actual, Is.EqualTo(new CPos[] { new(1, 2), new(3, 4), new(5, 6) }));
}
[Test]
public void GetValue_CVecArray()
{
var actual = FieldLoader.GetValue<CVec[]>("field", " 1 , 2 , 3 , 4 , 5 , 6 , , ");
Assert.That(actual, Is.EqualTo(new CVec[] { new(1, 2), new(3, 4), new(5, 6) }));
}
[Test]
public void GetValue_int2Array()
{
var actual = FieldLoader.GetValue<int2[]>("field", " 1 , 2 , 3 , 4 , 5 , 6 , , ");
Assert.That(actual, Is.EqualTo(new int2[] { new(1, 2), new(3, 4), new(5, 6) }));
}
[TestCase(null, null)]
[TestCase("", null)]
[TestCase("123", 123)]
public void GetValue_Nullable(string input, int? expected)
{
var actual = FieldLoader.GetValue<int?>("field", $" {input} ");
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(null, new int[] { })]
[TestCase("", new int[] { })]
[TestCase("1", new int[] { 1 })]
[TestCase("1,2,3", new int[] { 1, 2, 3 })]
[TestCase("1,,3", new int[] { 1, 3 })]
[TestCase(" 1 , 2 , 3 ", new int[] { 1, 2, 3 })]
[TestCase(" 1 , , 3 ", new int[] { 1, 3 })]
[TestCase("1,1,2,2,3", new int[] { 1, 1, 2, 2, 3 })]
[TestCase("1,1,1,1", new int[] { 1, 1, 1, 1 })]
public void GetValue_Array(string input, int[] expected)
{
var actual = FieldLoader.GetValue<int[]>("field", input);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(null, new int[] { })]
[TestCase("", new int[] { })]
[TestCase("1", new int[] { 1 })]
[TestCase("1,2,3", new int[] { 1, 2, 3 })]
[TestCase("1,,3", new int[] { 1, 3 })]
[TestCase(" 1 , 2 , 3 ", new int[] { 1, 2, 3 })]
[TestCase(" 1 , , 3 ", new int[] { 1, 3 })]
[TestCase("1,1,2,2,3", new int[] { 1, 1, 2, 2, 3 })]
[TestCase("1,1,1,1", new int[] { 1, 1, 1, 1 })]
public void GetValue_List(string input, int[] expected)
{
var actual = FieldLoader.GetValue<List<int>>("field", input);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(null, new int[] { })]
[TestCase("", new int[] { })]
[TestCase("1", new int[] { 1 })]
[TestCase("1,2,3", new int[] { 1, 2, 3 })]
[TestCase("1,,3", new int[] { 1, 3 })]
[TestCase(" 1 , 2 , 3 ", new int[] { 1, 2, 3 })]
[TestCase(" 1 , , 3 ", new int[] { 1, 3 })]
[TestCase("1,1,2,2,3", new int[] { 1, 2, 3 })]
[TestCase("1,1,1,1", new int[] { 1 })]
public void GetValue_HashSet(string input, int[] expected)
{
var actual = FieldLoader.GetValue<HashSet<int>>("field", input);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(null)]
[TestCase("")]
[TestCase("1")]
[TestCase("1,2,3")]
[TestCase("1,,3")]
[TestCase(" 1 , 2 , 3 ")]
[TestCase(" 1 , , 3 ")]
[TestCase("1,1,2,2,3")]
[TestCase("1,1,1,1")]
public void GetValue_BitSet(string input)
{
var actual = FieldLoader.GetValue<BitSet<FieldLoaderTest>>("field", input);
var expected = input == null
? new BitSet<FieldLoaderTest>([])
: new BitSet<FieldLoaderTest>(input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(null)]
[TestCase("")]
[TestCase("1")]
[TestCase("1,2")]
public void GetValue_Dictionary(string input)
{
var actual = FieldLoader.GetValue<Dictionary<int, int>>("field", input);
Assert.That(actual, Is.Empty);
}
[TestCase(TypeArgs = [typeof(int[][])])]
[TestCase(TypeArgs = [typeof(List<List<int>>)])]
[TestCase(TypeArgs = [typeof(HashSet<HashSet<int>>)])]
public void GetValue_NestedCollections<T>()
{
var actual = FieldLoader.GetValue<T>("field", "1,2,3");
Assert.That(actual, Is.EquivalentTo(new int[][] { [1], [2], [3] }));
}
[Test]
public void GetValue_TypeConverter()
{
// We don't have hardcoded handling for sbyte, but a TypeConverter for it does exist.
var actual = FieldLoader.GetValue<sbyte>("field", " 123 ");
Assert.That(actual, Is.EqualTo(123));
}
[Test]
public void GetValue_TypeConverter_Invalid()
{
static void Act() => FieldLoader.GetValue<sbyte>("field", " test ");
Assert.That(Act, Throws.TypeOf<YamlException>().And.Message.EqualTo($"FieldLoader: Cannot parse `test` into `field.{typeof(sbyte).FullName}`"));
}
sealed class LoadFieldOrPropertyTarget
{
#pragma warning disable IDE0044 // Add readonly modifier
#pragma warning disable RCS1170 // Use read-only auto-implemented property
int privateIntField;
int PrivateIntProp { get; set; }
public int PublicIntField;
public int PublicIntProp { get; set; }
public int GetPrivateIntField() => privateIntField;
public int GetPrivateIntProp() => PrivateIntProp;
#pragma warning restore IDE0044 // Add readonly modifier
#pragma warning restore RCS1170 // Use read-only auto-implemented property
}
[Test]
public void LoadFieldOrProperty()
{
var target = new LoadFieldOrPropertyTarget();
FieldLoader.LoadFieldOrProperty(target, $" {nameof(LoadFieldOrPropertyTarget.PublicIntField)} ", "12");
FieldLoader.LoadFieldOrProperty(target, $" {nameof(LoadFieldOrPropertyTarget.PublicIntProp)} ", "34");
FieldLoader.LoadFieldOrProperty(target, " privateIntField ", "56");
FieldLoader.LoadFieldOrProperty(target, " PrivateIntProp ", "78");
void Act() => FieldLoader.LoadFieldOrProperty(target, "unknown", "");
Assert.That(target.PublicIntField, Is.EqualTo(12));
Assert.That(target.PublicIntProp, Is.EqualTo(34));
Assert.That(target.GetPrivateIntField(), Is.EqualTo(56));
Assert.That(target.GetPrivateIntProp(), Is.EqualTo(78));
Assert.That(Act, Throws.TypeOf<NotImplementedException>().And.Message.EqualTo("FieldLoader: Missing field `unknown` on `LoadFieldOrPropertyTarget`"));
}
sealed class LoadTarget
{
public int Int;
public string String;
public string Unset;
}
[Test]
public void Load()
{
var target = new LoadTarget() { Unset = "unset" };
var yaml = new MiniYaml(
null,
[
new MiniYamlNode(nameof(LoadTarget.Int), "123"),
new MiniYamlNode(nameof(LoadTarget.String), "test"),
]);
FieldLoader.Load(target, yaml);
Assert.That(target.Int, Is.EqualTo(123));
Assert.That(target.String, Is.EqualTo("test"));
Assert.That(target.Unset, Is.EqualTo("unset"));
}
[Test]
public void Load_Generic()
{
var expected = new LoadTarget();
var yaml = new MiniYaml(
null,
[
new MiniYamlNode(nameof(LoadTarget.Int), "123"),
new MiniYamlNode(nameof(LoadTarget.String), "test"),
]);
FieldLoader.Load(expected, yaml);
var actual = FieldLoader.Load<LoadTarget>(yaml);
Assert.That(actual.Int, Is.EqualTo(expected.Int));
Assert.That(actual.String, Is.EqualTo(expected.String));
Assert.That(actual.Unset, Is.EqualTo(expected.Unset));
}
sealed class LoadDictionaryTarget
{
public Dictionary<int, int> Dictionary;
}
[Test]
public void Load_Dictionary()
{
var target = new LoadDictionaryTarget();
var yaml = new MiniYaml(
null,
[
new MiniYamlNode(
nameof(LoadDictionaryTarget.Dictionary),
new MiniYaml(
null,
[
new MiniYamlNode("12", "34"),
new MiniYamlNode("56", "78")
]))
]);
FieldLoader.Load(target, yaml);
Assert.That(target.Dictionary, Is.EquivalentTo(new Dictionary<int, int> { { 12, 34 }, { 56, 78 } }));
}
sealed class LoadRequiredTarget
{
[FieldLoader.Require]
public int Int1 = 1;
[FieldLoader.Require]
public int Int2 = 2;
[FieldLoader.Require]
public int Int3 = 3;
public int Int4 = 4;
public int Int5 = 5;
}
[Test]
public void Load_Required()
{
var target = new LoadRequiredTarget();
var yaml = new MiniYaml(
null,
[
new MiniYamlNode(nameof(LoadRequiredTarget.Int1), "123"),
new MiniYamlNode(nameof(LoadRequiredTarget.Int4), "456"),
]);
void Act() => FieldLoader.Load(target, yaml);
Assert.That(Act,
Throws.TypeOf<FieldLoader.MissingFieldsException>().And
.Message.EqualTo($"{nameof(LoadRequiredTarget.Int2)}, {nameof(LoadRequiredTarget.Int3)}"));
Assert.That(target.Int1, Is.EqualTo(123));
Assert.That(target.Int2, Is.EqualTo(2));
Assert.That(target.Int3, Is.EqualTo(3));
Assert.That(target.Int4, Is.EqualTo(456));
Assert.That(target.Int5, Is.EqualTo(5));
}
sealed class LoadIgnoreTarget
{
[FieldLoader.Ignore]
public int Int1 = 1;
[FieldLoader.Ignore]
public int Int2 = 2;
public int Int3 = 3;
}
[Test]
public void Load_Ignore()
{
var target = new LoadIgnoreTarget();
var yaml = new MiniYaml(
null,
[
new MiniYamlNode(nameof(LoadIgnoreTarget.Int1), "123"),
new MiniYamlNode(nameof(LoadIgnoreTarget.Int3), "456"),
]);
FieldLoader.Load(target, yaml);
Assert.That(target.Int1, Is.EqualTo(1));
Assert.That(target.Int2, Is.EqualTo(2));
Assert.That(target.Int3, Is.EqualTo(456));
}
sealed class LoadUsingTarget
{
[FieldLoader.LoadUsing(nameof(LoadInt))]
public int Int1 = 1;
[FieldLoader.LoadUsing(nameof(LoadInt), true)]
public int Int2 = 2;
[FieldLoader.LoadUsing(nameof(LoadInt))]
public int Int3 = 3;
[FieldLoader.LoadUsing(nameof(LoadInt), true)]
public int Int4 = 4;
public int Int5 = 5;
static object LoadInt(MiniYaml yaml) => Exts.ParseInt32Invariant(yaml.NodeWithKey("ForLoadUsing").Value.Value);
}
[Test]
public void Load_Using()
{
var target = new LoadUsingTarget();
var yaml = new MiniYaml(
null,
[
new MiniYamlNode("ForLoadUsing", "100"),
new MiniYamlNode(nameof(LoadUsingTarget.Int1), "12"),
new MiniYamlNode(nameof(LoadUsingTarget.Int2), "34"),
new MiniYamlNode(nameof(LoadUsingTarget.Int5), "56"),
]);
void Act() => FieldLoader.Load(target, yaml);
Assert.That(Act,
Throws.TypeOf<FieldLoader.MissingFieldsException>().And
.Message.EqualTo(nameof(LoadRequiredTarget.Int4)));
Assert.That(target.Int1, Is.EqualTo(100));
Assert.That(target.Int2, Is.EqualTo(100));
Assert.That(target.Int3, Is.EqualTo(100));
Assert.That(target.Int4, Is.EqualTo(4));
Assert.That(target.Int5, Is.EqualTo(56));
}
sealed class LoadUsingMissingTarget
{
[FieldLoader.LoadUsing("unknown")]
public int Int = 1;
}
[Test]
public void Load_UsingMissing()
{
var target = new LoadUsingMissingTarget();
var yaml = new MiniYaml(null);
void Act() => FieldLoader.Load(target, yaml);
Assert.That(Act,
Throws.TypeOf<InvalidOperationException>().And
.Message.EqualTo("LoadUsingMissingTarget does not specify a loader function 'unknown'"));
Assert.That(target.Int, Is.EqualTo(1));
}
}
}

View File

@@ -0,0 +1,265 @@
#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA.Test
{
[TestFixture]
sealed class FieldSaverTest
{
static IEnumerable<TestCaseData> FormatValue_Primitive_TestCases()
{
return
[
new TestCaseData(123),
new TestCaseData((ushort)123),
new TestCaseData(123L),
new TestCaseData(123.4f),
new TestCaseData(123m),
new TestCaseData("test"),
new TestCaseData(Color.CornflowerBlue),
new TestCaseData(new Hotkey(Keycode.A, Modifiers.Shift)),
new TestCaseData(new WDist(123)),
new TestCaseData(new WVec(123, 456, 789)),
new TestCaseData(new WPos(123, 456, 789)),
new TestCaseData(new WAngle(123)),
new TestCaseData(new WRot(new WAngle(123), new WAngle(456), new WAngle(789))),
new TestCaseData(new CPos(123, 456)),
new TestCaseData(new CPos(123, 456, 78)),
new TestCaseData(new CVec(123, 456)),
new TestCaseData(new BooleanExpression("true")),
new TestCaseData(new IntegerExpression("1 + 2")),
new TestCaseData(MapGridType.RectangularIsometric),
new TestCaseData(SystemActors.World | SystemActors.EditorWorld),
new TestCaseData(true),
new TestCaseData(new Size(123, 456)),
new TestCaseData(new int2(123, 456)),
new TestCaseData(new float2(123, 456)),
new TestCaseData(new float3(123, 456, 789)),
new TestCaseData(new Rectangle(123, 456, 789, 123)),
];
}
[TestCaseSource(nameof(FormatValue_Primitive_TestCases))]
public void FormatVaue_Primitive<T>(T expected)
{
var actual = FieldSaver.FormatValue(expected);
Assert.That(actual, Is.EqualTo(expected.ToString()));
}
[Test]
public void FormatValue_Null()
{
var actual = FieldSaver.FormatValue(null);
Assert.That(actual, Is.EqualTo(""));
}
[Test]
public void FormatValue_DateTime()
{
var input = new DateTime(2000, 1, 1);
var actual = FieldSaver.FormatValue(input);
Assert.That(actual, Is.EqualTo(input.ToString("yyyy-MM-dd HH-mm-ss", CultureInfo.InvariantCulture)));
}
[Test]
public void FormatValue_WVecArray()
{
var actual = FieldSaver.FormatValue(new WVec[] { new(1, 2, 3), new(4, 5, 6) });
Assert.That(actual, Is.EqualTo("1,2,3, 4,5,6"));
}
[Test]
public void FormatValue_CPosArray()
{
var actual = FieldSaver.FormatValue(new CPos[] { new(1, 2), new(3, 4), new(5, 6) });
Assert.That(actual, Is.EqualTo("1,2, 3,4, 5,6"));
}
[Test]
public void FormatValue_CVecArray()
{
var actual = FieldSaver.FormatValue(new CVec[] { new(1, 2), new(3, 4), new(5, 6) });
Assert.That(actual, Is.EqualTo("1,2, 3,4, 5,6"));
}
[Test]
public void FormatValue_int2Array()
{
var actual = FieldSaver.FormatValue(new int2[] { new(1, 2), new(3, 4), new(5, 6) });
Assert.That(actual, Is.EqualTo("1,2, 3,4, 5,6"));
}
[TestCase(null, "")]
[TestCase(123, "123")]
public void FormatValue_Nullable(int? input, string expected)
{
var actual = FieldSaver.FormatValue(input);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(null, "")]
[TestCase(new int[] { }, "")]
[TestCase(new int[] { 1 }, "1")]
[TestCase(new int[] { 1, 2, 3 }, "1, 2, 3")]
[TestCase(new int[] { 1, 1, 2, 2, 3 }, "1, 1, 2, 2, 3")]
[TestCase(new int[] { 1, 1, 1, 1 }, "1, 1, 1, 1")]
public void FormatValue_Array(int[] input, string expected)
{
var actual = FieldSaver.FormatValue(input);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(null, "")]
[TestCase(new int[] { }, "")]
[TestCase(new int[] { 1 }, "1")]
[TestCase(new int[] { 1, 2, 3 }, "1, 2, 3")]
[TestCase(new int[] { 1, 1, 2, 2, 3 }, "1, 1, 2, 2, 3")]
[TestCase(new int[] { 1, 1, 1, 1 }, "1, 1, 1, 1")]
public void FormatValue_List(int[] input, string expected)
{
var actual = FieldSaver.FormatValue(input?.ToList());
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(null, "")]
[TestCase(new int[] { }, "")]
[TestCase(new int[] { 1 }, "1")]
[TestCase(new int[] { 1, 2, 3 }, "1, 2, 3")]
[TestCase(new int[] { 1, 1, 2, 2, 3 }, "1, 2, 3")]
[TestCase(new int[] { 1, 1, 1, 1 }, "1")]
public void FormatValue_HashSet(int[] input, string expected)
{
var actual = FieldSaver.FormatValue(input?.ToHashSet());
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("")]
[TestCase("1")]
[TestCase("1,2,3")]
[TestCase("1,1,2,2,3")]
[TestCase("1,1,1,1")]
public void FormatValue_BitSet(string input)
{
var actual = FieldSaver.FormatValue(new BitSet<FieldSaverTest>(input));
Assert.That(actual, Is.EqualTo(input));
}
[Test]
public void FormatValue_Dictionary()
{
var input = new Dictionary<int, int> { { 12, 34 }, { 56, 78 } };
var actual = FieldSaver.FormatValue(input);
Assert.That(actual, Is.EqualTo($"12: 34{Environment.NewLine}56: 78{Environment.NewLine}"));
}
sealed class FieldTarget
{
public int Int = 123;
}
[Test]
public void FormatValue_Field()
{
var actual = FieldSaver.FormatValue(new FieldTarget(), typeof(FieldTarget).GetField(nameof(FieldTarget.Int)));
Assert.That(actual, Is.EqualTo("123"));
}
[Test]
public void SaveField()
{
var actual = FieldSaver.SaveField(new FieldTarget(), nameof(FieldTarget.Int));
var actualString = MiniYamlExts.WriteToString([actual]);
var expectedString = MiniYamlExts.WriteToString([new MiniYamlNode(nameof(FieldTarget.Int), "123")]);
Assert.That(actualString, Is.EqualTo(expectedString));
}
sealed class SaveTarget
{
public int Int = 123;
public string String = "test";
public int[] IntArray = [1, 2, 3];
public Dictionary<string, string> StringDictionary = new() { { "a", "b" }, { "c", "d" } };
}
[Test]
public void Save()
{
var expected = new MiniYaml(
null,
[
new MiniYamlNode(nameof(SaveTarget.Int), "123"),
new MiniYamlNode(nameof(SaveTarget.String), "test"),
new MiniYamlNode(nameof(SaveTarget.IntArray), "1, 2, 3"),
new MiniYamlNode(
nameof(SaveTarget.StringDictionary),
new MiniYaml(null, [new MiniYamlNode("a", "b"), new MiniYamlNode("c", "d")])),
]);
var actual = FieldSaver.Save(new SaveTarget());
var actualString = MiniYamlExts.WriteToString(actual.Nodes);
var expectedString = MiniYamlExts.WriteToString(expected.Nodes);
Assert.That(actual.Value, Is.EqualTo(expected.Value));
Assert.That(actualString, Is.EqualTo(expectedString));
}
[Test]
public void SaveDifferences()
{
var expected = new MiniYaml(
null,
[
new MiniYamlNode(nameof(SaveTarget.String), ""),
new MiniYamlNode(nameof(SaveTarget.IntArray), "1, 2, 4"),
]);
var actual = FieldSaver.SaveDifferences(new SaveTarget { IntArray = [1, 2, 4], String = null }, new SaveTarget());
var actualString = MiniYamlExts.WriteToString(actual.Nodes);
var expectedString = MiniYamlExts.WriteToString(expected.Nodes);
Assert.That(actual.Value, Is.EqualTo(expected.Value));
Assert.That(actualString, Is.EqualTo(expectedString));
}
[Test]
public void SaveDifferences_DifferentTypes()
{
static void Act() => FieldSaver.SaveDifferences(new object(), new SaveTarget());
Assert.That(Act, Throws.TypeOf<InvalidOperationException>().And.Message.EqualTo("FieldSaver: can't diff objects of different types"));
}
}
}