Skip to content

Commit

Permalink
Use reflection to implement SettingsEntity.NewInstance rather than Bi…
Browse files Browse the repository at this point in the history
…naryFormatter
  • Loading branch information
borland committed Oct 24, 2023
1 parent 036ba68 commit 26954bd
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 13 deletions.
1 change: 1 addition & 0 deletions source/Nuke.Tooling.Tests/Nuke.Tooling.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<ItemGroup>
<ProjectReference Include="..\Nuke.Build.Shared\Nuke.Build.Shared.csproj" />
<ProjectReference Include="..\Nuke.Common\Nuke.Common.csproj" />
<ProjectReference Include="..\Nuke.Tooling\Nuke.Tooling.csproj" />
</ItemGroup>

Expand Down
160 changes: 160 additions & 0 deletions source/Nuke.Tooling.Tests/SettingsEntity.NewInstanceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright 2023 Maintainers of NUKE.
// Distributed under the MIT License.
// https://github.com/nuke-build/nuke/blob/master/LICENSE

using System;
using System.Collections.Generic;
using FluentAssertions;
using FluentAssertions.Execution;
using Nuke.Common.Tooling;
using Nuke.Common.Tools.OctoVersion;
using Xunit;

namespace Nuke.Common.Tests;

public class SettingsEntity_NewInstanceTest
{
private class SimpleSettings : ISettingsEntity
{
public string stringField;
public string StringProperty { get; set; }
public int intField;
public int IntProperty { get; set; }
}

[Fact]
public void ClonesSimpleSettingsType()
{
var s = new SimpleSettings { stringField = "sf", StringProperty = "SP", intField = 1, IntProperty = 10 };
var s2 = s.NewInstance();

s2.Should().BeEquivalentTo(s);
}

private class ComplexSettings : ISettingsEntity
{
public class ComplexChildInit
{
public string Name { get; init; }
public DateTimeOffset DateOfBirth { get; init; }
}

public class ComplexChildCtor
{
public ComplexChildCtor(string name, DateTimeOffset dateOfBirth)
{
Name = name;
DateOfBirth = dateOfBirth;
}

public string Name { get; }
public DateTimeOffset DateOfBirth { get; init; }
}

public record ComplexChildRecord(string[] Names, double Average);

public ComplexSettings(Dictionary<string, ComplexChildCtor> childCtor, ComplexChildRecord childRecord)
{
ChildCtor = childCtor;
ChildRecord = childRecord;
}

public ComplexChildInit ChildInit { get; set; }
public Dictionary<string, ComplexChildCtor> ChildCtor { get; }
public ComplexChildRecord ChildRecord { get; }
}

[Fact]
public void ClonesComplexSettingsType()
{
var s = new ComplexSettings(
new Dictionary<string, ComplexSettings.ComplexChildCtor>
{
["a"] = new("a", DateTimeOffset.UnixEpoch),
["xyz"] = new("xyz", DateTimeOffset.UnixEpoch.AddDays(12))
},
new ComplexSettings.ComplexChildRecord(new[] { "Horse", "Cat", "Dog" }, Average: 7.5))
{ ChildInit = new ComplexSettings.ComplexChildInit { DateOfBirth = DateTimeOffset.FromUnixTimeSeconds(1698142718) } };
var s2 = s.NewInstance();

s2.Should().BeEquivalentTo(s);
}

private class PartiallySerializableSettings : ISettingsEntity
{
public string stringField;

[field: NonSerialized]
public string StringProperty { get; set; }

[NonSerialized]
public int intField;

public int IntProperty { get; set; }
}

[Fact]
public void IgnoresNonSerializableTypes()
{
var s = new PartiallySerializableSettings { stringField = "sf", StringProperty = "SP", intField = 1, IntProperty = 10 };
var s2 = s.NewInstance();

using var scope = new AssertionScope();

s2.stringField.Should().Be(s.stringField);
s2.StringProperty.Should().BeNull(); // nonserialized
s2.intField.Should().Be(0); // nonserialized
s2.IntProperty.Should().Be(s.IntProperty);
}

private class MyCustomToolSettings : ToolSettings
{
public string StringProperty { get; set; }
public int IntProperty { get; set; }
public Func<string, int> Calc { get; set; }
}

[Fact]
public void IgnoresFuncTypesExceptToolSettings()
{
var s = new MyCustomToolSettings { StringProperty = "Str", IntProperty = 12, Calc = s => s.Length * 2 };

var processLog = new List<(OutputType, string)>();

s.ProcessArgumentConfigurator = arguments => arguments;
s.ProcessLogger = (outputType, str) => processLog.Add((outputType, str));
s.ProcessExitHandler = (_, _) => { };
var s2 = s.NewInstance();

using var scope = new AssertionScope();

s2.StringProperty.Should().Be(s.StringProperty);
s2.IntProperty.Should().Be(s.IntProperty);
s2.Calc.Should().BeNull(); // can't copy a func

// except these 3 have special handling
s2.ProcessArgumentConfigurator.Should().NotBeNull();
s2.ProcessLogger.Should().NotBeNull();
s2.ProcessExitHandler.Should().NotBeNull();

s2.ProcessLogger(OutputType.Err, "an err");

processLog.Should().BeEquivalentTo(new (OutputType, string)[] { new(OutputType.Err, "an err") });
}

[Fact]
public void CanCloneOctoVersion() // at least one "real" type
{
var s = new OctoVersionInfo
{
BuildMetaData = "amazing",
MajorMinorPatch = "1.2.3",
Major = 1,
Minor = 2,
Patch = 3
};

var s2 = s.NewInstance();
s2.Should().BeEquivalentTo(s);
}
}
84 changes: 71 additions & 13 deletions source/Nuke.Tooling/SettingsEntity.NewInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
// https://github.com/nuke-build/nuke/blob/master/LICENSE

using System;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Reflection;
using System.Runtime.Serialization;
using JetBrains.Annotations;
#pragma warning disable SYSLIB0011

namespace Nuke.Common.Tooling;

Expand All @@ -17,20 +16,79 @@ public static partial class SettingsEntityExtensions
public static T NewInstance<T>(this T settingsEntity)
where T : ISettingsEntity
{
var binaryFormatter = new BinaryFormatter();

using var memoryStream = new MemoryStream();
binaryFormatter.Serialize(memoryStream, settingsEntity);
memoryStream.Seek(offset: 0, loc: SeekOrigin.Begin);

var newInstance = (T) binaryFormatter.Deserialize(memoryStream);
var newInstance = (T)CloneUsingReflection(settingsEntity);
if (newInstance is ToolSettings toolSettings)
{
toolSettings.ProcessArgumentConfigurator = ((ToolSettings) (object) settingsEntity).ProcessArgumentConfigurator;
toolSettings.ProcessLogger = ((ToolSettings) (object) settingsEntity).ProcessLogger;
toolSettings.ProcessExitHandler = ((ToolSettings) (object) settingsEntity).ProcessExitHandler;
toolSettings.ProcessArgumentConfigurator = ((ToolSettings)(object)settingsEntity).ProcessArgumentConfigurator;
toolSettings.ProcessLogger = ((ToolSettings)(object)settingsEntity).ProcessLogger;
toolSettings.ProcessExitHandler = ((ToolSettings)(object)settingsEntity).ProcessExitHandler;
}

return newInstance;
}

internal static object CloneUsingReflection(object original)
{
if (original == null)
{
return null;
}

// we have to use the runtime type of the object, not the declared type
var type = original.GetType();

// known-blittable types
if (type.IsPrimitive || type.IsEnum || type == typeof(string))
{
return original;
}

if (type.IsArray)
{
if (type.GetArrayRank() != 1)
{
throw new NotSupportedException("Multidimensional arrays not supported");
}

var originalArray = (Array)original;
var arrayElementType = type.GetElementType()!;
var clone = Array.CreateInstance(arrayElementType, originalArray.Length);
for (var i = 0; i < originalArray.Length; i++)
{
clone.SetValue(CloneUsingReflection(originalArray.GetValue(i)), i);
}

return clone;
}
else
{
// first check if it's a Delegate as we can't clone those
var baseType = type.BaseType;
while (baseType?.BaseType != typeof(object) && baseType?.BaseType != null)
{
baseType = baseType.BaseType;
}

if (baseType != null && baseType == typeof(Delegate))
{
return null;
}

// now clone using GetSafeUninitializedObject
var clone = FormatterServices.GetSafeUninitializedObject(type);
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
// don't need to iterate over Properties as Fields will include the backing stores for any properties
if (field.GetCustomAttribute(typeof(NonSerializedAttribute)) != null)
{
continue; // skip NonSerialized fields
}

var value = field.GetValue(original);
field.SetValue(clone, CloneUsingReflection(value));
}

return clone;
}
}
}

0 comments on commit 26954bd

Please sign in to comment.