[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/sdpython/pythonnet/master/src/embed_tests/TestDomainReload.cs [Back]  [Original]

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(); // 

Web Proxy Viewer  |  New URL  |  Original Page