GitHub Viewer
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using NUnit.Framework;
using Python.Runtime;
using PyRuntime = Python.Runtime.Runtime;
//
// This test case is disabled on .NET Standard because it doesn't have all the
// APIs we use. We could work around that, but .NET Core doesn't implement
// domain creation, so it's not worth it.
//
// Unfortunately this means no continuous integration testing for this case.
//
#if NETFRAMEWORK
namespace Python.EmbeddingTest
{
class TestDomainReload
{
abstract class CrossCaller : MarshalByRefObject
{
public abstract ValueType Execute(ValueType arg);
}
///
/// Test that the python runtime can survive a C# domain reload without crashing.
///
/// At the time this test was written, there was a very annoying
/// seemingly random crash bug when integrating pythonnet into Unity.
///
/// The repro steps that David Lassonde, Viktoria Kovecses and
/// Benoit Hudson eventually worked out:
/// 1. Write a HelloWorld.cs script that uses Python.Runtime to access
/// some C# data from python: C# calls python, which calls C#.
/// 2. Execute the script (e.g. make it a MenuItem and click it).
/// 3. Touch HelloWorld.cs on disk, forcing Unity to recompile scripts.
/// 4. Wait several seconds for Unity to be done recompiling and
/// reloading the C# domain.
/// 5. Make python run the gc (e.g. by calling gc.collect()).
///
/// The reason:
/// A. In step 2, Python.Runtime registers a bunch of new types with
/// their tp_traverse slot pointing to managed code, and allocates
/// some objects of those types.
/// B. In step 4, Unity unloads the C# domain. That frees the managed
/// code. But at the time of the crash investigation, pythonnet
/// leaked the python side of the objects allocated in step 1.
/// C. In step 5, python sees some pythonnet objects in its gc list of
/// potentially-leaked objects. It calls tp_traverse on those objects.
/// But tp_traverse was freed in step 3 => CRASH.
///
/// This test distills what's going on without needing Unity around (we'd see
/// similar behaviour if we were using pythonnet on a .NET web server that did
/// a hot reload).
///
[Test]
public static void DomainReloadAndGC()
{
Assert.IsFalse(PythonEngine.IsInitialized);
RunAssemblyAndUnload("test1");
Assert.That(PyRuntime.Py_IsInitialized() != 0,
"On soft-shutdown mode, Python runtime should still running");
RunAssemblyAndUnload("test2");
Assert.That(PyRuntime.Py_IsInitialized() != 0,
"On soft-shutdown mode, Python runtime should still running");
if (PythonEngine.DefaultShutdownMode == ShutdownMode.Normal)
{
// The default mode is a normal mode,
// it should shutdown the Python VM avoiding influence other tests.
PyRuntime.PyGILState_Ensure();
PyRuntime.Py_Finalize();
}
}
#region CrossDomainObject
class CrossDomainObjectStep1 : CrossCaller
{
public override ValueType Execute(ValueType arg)
{
try
{
// Create a C# user-defined object in Python. Asssing some values.
Type type = typeof(Python.EmbeddingTest.Domain.MyClass);
string code = string.Format(@"
import clr
clr.AddReference('{0}')
from Python.EmbeddingTest.Domain import MyClass
obj = MyClass()
obj.Method()
obj.StaticMethod()
obj.Property = 1
obj.Field = 10
", Assembly.GetExecutingAssembly().FullName);
using (Py.GIL())
using (var scope = Py.CreateScope())
{
scope.Exec(code);
using (PyObject obj = scope.Get("obj"))
{
Debug.Assert(obj.AsManagedObject(type).GetType() == type);
// We only needs its Python handle
PyRuntime.XIncref(obj.Handle);
return obj.Handle;
}
}
}
catch (Exception e)
{
Debug.WriteLine(e);
throw;
}
}
}
class CrossDomainObjectStep2 : CrossCaller
{
public override ValueType Execute(ValueType arg)
{
// handle refering a clr object created in previous domain,
// it should had been deserialized and became callable agian.
IntPtr handle = (IntPtr)arg;
try
{
using (Py.GIL())
{
IntPtr tp = Runtime.Runtime.PyObject_TYPE(handle);
IntPtr tp_clear = Marshal.ReadIntPtr(tp, TypeOffset.tp_clear);
Assert.That(tp_clear, Is.Not.Null);
using (PyObject obj = new PyObject(handle))
{
obj.InvokeMethod("Method");
obj.InvokeMethod("StaticMethod");
using (var scope = Py.CreateScope())
{
scope.Set("obj", obj);
scope.Exec(@"
obj.Method()
obj.StaticMethod()
obj.Property += 1
obj.Field += 10
");
}
var clrObj = obj.As();
Assert.AreEqual(clrObj.Property, 2);
Assert.AreEqual(clrObj.Field, 20);
}
}
}
catch (Exception e)
{
Debug.WriteLine(e);
throw;
}
return 0;
}
}
///
/// Create a C# custom object in a domain, in python code.
/// Unload the domain, create a new domain.
/// Make sure the C# custom object created in the previous domain has been re-created
///
[Test]
public static void CrossDomainObject()
{
RunDomainReloadSteps();
}
#endregion
#region Tempary tests
// https://github.com/pythonnet/pythonnet/pull/1074#issuecomment-596139665
[Test]
public void CrossReleaseBuiltinType()
{
void ExecTest()
{
try
{
PythonEngine.Initialize();
var numRef = CreateNumReference();
Assert.True(numRef.IsAlive);
PythonEngine.Shutdown(); //