using NUnit.Framework;
using Python.Runtime;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
namespace Python.EmbeddingTest
{
public class TestFinalizer
{
private int _oldThreshold;
[SetUp]
public void SetUp()
{
_oldThreshold = Finalizer.Instance.Threshold;
PythonEngine.Initialize();
Exceptions.Clear();
}
[TearDown]
public void TearDown()
{
Finalizer.Instance.Threshold = _oldThreshold;
PythonEngine.Shutdown();
}
private static void FullGCCollect()
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
[Test]
[Obsolete("GC tests are not guaranteed")]
public void CollectBasicObject()
{
Assert.IsTrue(Finalizer.Instance.Enable);
Finalizer.Instance.Threshold = 1;
bool called = false;
var objectCount = 0;
EventHandler handler = (s, e) =>
{
objectCount = e.ObjectCount;
called = true;
};
Assert.IsFalse(called, "The event handler was called before it was installed");
Finalizer.Instance.CollectOnce += handler;
IntPtr pyObj = MakeAGarbage(out var shortWeak, out var longWeak);
FullGCCollect();
// The object has been resurrected
Warn.If(
shortWeak.IsAlive,
"The referenced object is alive although it should have been collected",
shortWeak
);
Assert.IsTrue(
longWeak.IsAlive,
"The reference object is not alive although it should still be",
longWeak
);
{
var garbage = Finalizer.Instance.GetCollectedObjects();
Assert.NotZero(garbage.Count, "There should still be garbage around");
Warn.Unless(
garbage.Contains(pyObj),
$"The {nameof(longWeak)} reference doesn't show up in the garbage list",
garbage
);
}
try
{
Finalizer.Instance.Collect();
}
finally
{
Finalizer.Instance.CollectOnce -= handler;
}
Assert.IsTrue(called, "The event handler was not called during finalization");
Assert.GreaterOrEqual(objectCount, 1);
}
[Test]
[Obsolete("GC tests are not guaranteed")]
public void CollectOnShutdown()
{
IntPtr op = MakeAGarbage(out var shortWeak, out var longWeak);
FullGCCollect();
Assert.IsFalse(shortWeak.IsAlive);
List garbage = Finalizer.Instance.GetCollectedObjects();
Assert.IsNotEmpty(garbage, "The garbage object should be collected");
Assert.IsTrue(garbage.Contains(op),
"Garbage should contains the collected object");
PythonEngine.Shutdown();
garbage = Finalizer.Instance.GetCollectedObjects();
Assert.IsEmpty(garbage);
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] // ensure lack of references to obj
[Obsolete("GC tests are not guaranteed")]
private static IntPtr MakeAGarbage(out WeakReference shortWeak, out WeakReference longWeak)
{
IntPtr handle = IntPtr.Zero;
WeakReference @short = null, @long = null;
// must create Python object in the thread where we have GIL
IntPtr val = PyLong.FromLong(1024);
// must create temp object in a different thread to ensure it is not present
// when conservatively scanning stack for GC roots.
// see https://xamarin.github.io/bugzilla-archives/17/17593/bug.html
var garbageGen = new Thread(() =>
{
var obj = new PyObject(val, skipCollect: true);
@short = new WeakReference(obj);
@long = new WeakReference(obj, true);
handle = obj.Handle;
});
garbageGen.Start();
Assert.IsTrue(garbageGen.Join(TimeSpan.FromSeconds(5)), "Garbage creation timed out");
shortWeak = @short;
longWeak = @long;
return handle;
}
private static long CompareWithFinalizerOn(PyObject pyCollect, bool enbale)
{
// Must larger than 512 bytes make sure Python use
string str = new string('1', 1024);
Finalizer.Instance.Enable = true;
FullGCCollect();
FullGCCollect();
pyCollect.Invoke();
Finalizer.Instance.Collect();
Finalizer.Instance.Enable = enbale;
// Estimate unmanaged memory size
long before = Environment.WorkingSet - GC.GetTotalMemory(true);
for (int i = 0; i < 10000; i++)
{
// Memory will leak when disable Finalizer
new PyString(str);
}
FullGCCollect();
FullGCCollect();
pyCollect.Invoke();
if (enbale)
{
Finalizer.Instance.Collect();
}
FullGCCollect();
FullGCCollect();
long after = Environment.WorkingSet - GC.GetTotalMemory(true);
return after - before;
}
///
/// Because of two vms both have their memory manager,
/// this test only prove the finalizer has take effect.
///
[Test]
[Ignore("Too many uncertainties, only manual on when debugging")]
public void SimpleTestMemory()
{
bool oldState = Finalizer.Instance.Enable;
try
{
using (PyObject gcModule = PythonEngine.ImportModule("gc"))
using (PyObject pyCollect = gcModule.GetAttr("collect"))
{
long span1 = CompareWithFinalizerOn(pyCollect, false);
long span2 = CompareWithFinalizerOn(pyCollect, true);
Assert.Less(span2, span1);
}
}
finally
{
Finalizer.Instance.Enable = oldState;
}
}
[Test]
public void ValidateRefCount()
{
if (!Finalizer.Instance.RefCountValidationEnabled)
{
Assert.Ignore("Only run with FINALIZER_CHECK");
}
IntPtr ptr = IntPtr.Zero;
bool called = false;
Finalizer.IncorrectRefCntHandler handler = (s, e) =>
{
called = true;
Assert.AreEqual(ptr, e.Handle);
Assert.AreEqual(2, e.ImpactedObjects.Count);
// Fix for this test, don't do this on general environment
Runtime.Runtime.XIncref(e.Handle);
return false;
};
Finalizer.Instance.IncorrectRefCntResolver += handler;
try
{
ptr = CreateStringGarbage();
FullGCCollect();
Assert.Throws(() => Finalizer.Instance.Collect());
Assert.IsTrue(called);
}
finally
{
Finalizer.Instance.IncorrectRefCntResolver -= handler;
}
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] // ensure lack of references to s1 and s2
private static IntPtr CreateStringGarbage()
{
PyString s1 = new PyString("test_string");
// s2 steal a reference from s1
PyString s2 = new PyString(s1.Handle);
return s1.Handle;
}
}
}