From ffbe1e14dad346ef9cbe2e5e987646a112e2ed68 Mon Sep 17 00:00:00 2001 From: CoreyAlexander Date: Mon, 16 Dec 2024 08:11:34 -0500 Subject: [PATCH] feat: Add WeakReference to store reference types in a default provider --- .../dependent/DependencyResolver.cs | 53 +++++++++++++++---- .../auto_inject/dependent/DependentState.cs | 2 +- .../src/auto_inject/dependent/IDependent.cs | 2 +- .../test/fixtures/Dependents.cs | 19 +++++++ .../test/src/MiscTest.cs | 2 +- .../test/src/ResolutionTest.cs | 16 +++++- 6 files changed, 81 insertions(+), 13 deletions(-) diff --git a/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependencyResolver.cs b/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependencyResolver.cs index f7520aa..e182af1 100644 --- a/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependencyResolver.cs +++ b/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependencyResolver.cs @@ -129,7 +129,9 @@ public static TValue DependOn( // all the other dependency resolution methods. var state = dependent.MixinState.Get(); if (state.ProviderFakes.TryGetValue(typeof(TValue), out var fakeProvider)) { - return fakeProvider.Value(); + if (fakeProvider is IProvide faker) { + return faker.Value(); + } } // Lookup dependency, per usual, respecting any fallback values if there @@ -145,15 +147,16 @@ public static TValue DependOn( if (providerNode is IProvide provider) { return provider.Value(); } - else if (providerNode is DefaultProvider defaultProvider) { + else if (providerNode is DefaultProvider defaultProvider) { return defaultProvider.Value(); } } else if (fallback is not null) { // See if we were given a fallback. - var provider = new DefaultProvider(fallback()); + var value = fallback(); + var provider = new DefaultProvider(value, fallback); state.Dependencies.Add(typeof(TValue), provider); - return (TValue)provider.Value(); + return provider.Value(); } throw new ProviderNotFoundException(typeof(TValue)); @@ -283,15 +286,47 @@ void onProviderInitialized(IBaseProvider provider) { // for fallback values. } - public class DefaultProvider : IBaseProvider { - private readonly dynamic _value; + public class DefaultProvider : IBaseProvider { + private object _value; + private readonly Func? _fallback; public ProviderState ProviderState { get; } - public DefaultProvider(dynamic value) { - _value = value; + // When working with reference types, we must wrap the value in a WeakReference<>() + // to allow the garbage collection to work when the assembly is being unloaded or reloaded; + // such as in the case of rebuilding within the Godot Editor if you've instantiated a node + // and run it as a tool script. + public DefaultProvider(object value, Func? fallback = default) { + _fallback = fallback; + + if (value == null) { + throw new ArgumentNullException(nameof(fallback), "Value cannot be null."); + } + + _value = value.GetType().IsValueType ? value : new WeakReference(value); ProviderState = new() { IsInitialized = true }; } - public dynamic Value() => _value; + public TValue Value() { + if (_value is WeakReference weakReference) { + //Try to return a reference type. + if (weakReference.TryGetTarget(out var target)) { + return (TValue)target; + } + else { + //If value was garbage collected, make a new one. + if (_fallback == null) { + throw new InvalidOperationException("Fallback method is null."); + } + + var value = _fallback() ?? throw new InvalidOperationException("Fallback cannot create a null value"); + _value = new WeakReference(value); + return value; + } + } + else { + //Return a value type. + return (TValue)_value; + } + } } } diff --git a/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependentState.cs b/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependentState.cs index 2d746dc..2b3e8bf 100644 --- a/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependentState.cs +++ b/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/DependentState.cs @@ -51,7 +51,7 @@ public class DependentState { /// allows nodes being unit-tested to provide fake providers during unit tests /// that return mock or faked values. /// - public Dictionary ProviderFakes { + public DependencyResolver.DependencyTable ProviderFakes { get; } = []; } diff --git a/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/IDependent.cs b/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/IDependent.cs index f34c19f..29774f1 100644 --- a/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/IDependent.cs +++ b/Chickensoft.AutoInject.Tests/src/auto_inject/dependent/IDependent.cs @@ -98,6 +98,6 @@ this is IIntrospective introspective && public void FakeDependency(T value) where T : notnull { AddStateIfNeeded(); MixinState.Get().ProviderFakes[typeof(T)] = - new DependencyResolver.DefaultProvider(value); + new DependencyResolver.DefaultProvider(value); } } diff --git a/Chickensoft.AutoInject.Tests/test/fixtures/Dependents.cs b/Chickensoft.AutoInject.Tests/test/fixtures/Dependents.cs index 589b7bd..7cd24ac 100644 --- a/Chickensoft.AutoInject.Tests/test/fixtures/Dependents.cs +++ b/Chickensoft.AutoInject.Tests/test/fixtures/Dependents.cs @@ -56,6 +56,25 @@ public void OnResolved() { } } +[Meta(typeof(IAutoOn), typeof(IDependent))] +public partial class ReferenceDependentFallback : Node { + public override void _Notification(int what) => this.Notify(what); + + [Dependency] + public object MyDependency => this.DependOn(() => FallbackValue); + + public object FallbackValue { get; set; } = null!; + public bool OnResolvedCalled { get; private set; } + public object ResolvedValue { get; set; } = null!; + + public void OnReady() { } + + public void OnResolved() { + OnResolvedCalled = true; + ResolvedValue = MyDependency; + } +} + [Meta(typeof(IAutoOn), typeof(IDependent))] public partial class IntDependent : Node { public override void _Notification(int what) => this.Notify(what); diff --git a/Chickensoft.AutoInject.Tests/test/src/MiscTest.cs b/Chickensoft.AutoInject.Tests/test/src/MiscTest.cs index 58223a9..7ebf576 100644 --- a/Chickensoft.AutoInject.Tests/test/src/MiscTest.cs +++ b/Chickensoft.AutoInject.Tests/test/src/MiscTest.cs @@ -47,7 +47,7 @@ public void IDependentOnResolvedDoesNothing() { [Test] public void DefaultProviderState() { - var defaultProvider = new DependencyResolver.DefaultProvider("hi"); + var defaultProvider = new DependencyResolver.DefaultProvider("hi"); defaultProvider.ProviderState.ShouldNotBeNull(); } } diff --git a/Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs b/Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs index b5e825f..50185e7 100644 --- a/Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs +++ b/Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs @@ -131,7 +131,7 @@ public void ThrowsWhenNoProviderFound() { } [Test] - public void UsesFallbackValueWhenNoProviderFound() { + public void UsesValueFallbackValueWhenNoProviderFound() { var fallback = "Hello, world!"; var dependent = new StringDependentFallback { FallbackValue = fallback @@ -142,6 +142,20 @@ public void UsesFallbackValueWhenNoProviderFound() { dependent.ResolvedValue.ShouldBe(fallback); dependent.MyDependency.ShouldBe(fallback); } + + [Test] + public void UsesReferenceFallbackValueWhenNoProviderFound() { + var fallback = new Resource(); + var dependent = new ReferenceDependentFallback { + FallbackValue = fallback + }; + + dependent._Notification((int)Node.NotificationReady); + + dependent.ResolvedValue.ShouldBe(fallback); + dependent.MyDependency.ShouldBe(fallback); + } + [Test] public void ThrowsOnDependencyTableThatWasTamperedWith() { var fallback = "Hello, world!";