diff --git a/pythonnet.15.sln b/pythonnet.15.sln
index ce863817f..3f41b7abe 100644
--- a/pythonnet.15.sln
+++ b/pythonnet.15.sln
@@ -14,6 +14,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.Test.15", "src\testi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.PerformanceTests", "src\perf_tests\Python.PerformanceTests.csproj", "{6FB0D091-9CEC-4DCC-8701-C40F9BFC9EDE}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.DomainReloadTests.15", "src\domain_tests\Python.DomainReloadTests.15.csproj", "{F2FB6DA3-318E-4F30-9A1F-932C667E38C5}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Repo", "Repo", "{441A0123-F4C6-4EE4-9AEE-315FD79BE2D5}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
@@ -388,6 +390,65 @@ Global
{6FB0D091-9CEC-4DCC-8701-C40F9BFC9EDE}.ReleaseWinPY3|x64.Build.0 = ReleaseWinPY3|x64
{6FB0D091-9CEC-4DCC-8701-C40F9BFC9EDE}.ReleaseWinPY3|x86.ActiveCfg = ReleaseWinPY3|x86
{6FB0D091-9CEC-4DCC-8701-C40F9BFC9EDE}.ReleaseWinPY3|x86.Build.0 = ReleaseWinPY3|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|Any CPU.ActiveCfg = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|Any CPU.Build.0 = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x64.ActiveCfg = Debug|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x64.Build.0 = Debug|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x86.ActiveCfg = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x86.Build.0 = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|Any CPU.ActiveCfg = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|Any CPU.Build.0 = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|x64.ActiveCfg = Debug|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|x64.Build.0 = Debug|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|x86.ActiveCfg = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|x86.Build.0 = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|Any CPU.ActiveCfg = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|Any CPU.Build.0 = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|x64.ActiveCfg = Debug|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|x64.Build.0 = Debug|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|x86.ActiveCfg = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|x86.Build.0 = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|Any CPU.ActiveCfg = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|Any CPU.Build.0 = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|x64.ActiveCfg = Debug|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|x64.Build.0 = Debug|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|x86.ActiveCfg = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|x86.Build.0 = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|Any CPU.ActiveCfg = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|Any CPU.Build.0 = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|x64.ActiveCfg = Debug|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|x64.Build.0 = Debug|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|x86.ActiveCfg = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|x86.Build.0 = Debug|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|Any CPU.ActiveCfg = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x64.ActiveCfg = Release|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x64.Build.0 = Release|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x86.ActiveCfg = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x86.Build.0 = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|Any CPU.ActiveCfg = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|Any CPU.Build.0 = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|x64.ActiveCfg = Release|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|x64.Build.0 = Release|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|x86.ActiveCfg = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|x86.Build.0 = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|Any CPU.ActiveCfg = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|Any CPU.Build.0 = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|x64.ActiveCfg = Release|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|x64.Build.0 = Release|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|x86.ActiveCfg = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|x86.Build.0 = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|Any CPU.ActiveCfg = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|Any CPU.Build.0 = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|x64.ActiveCfg = Release|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|x64.Build.0 = Release|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|x86.ActiveCfg = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|x86.Build.0 = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|Any CPU.ActiveCfg = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|Any CPU.Build.0 = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|x64.ActiveCfg = Release|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|x64.Build.0 = Release|x64
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|x86.ActiveCfg = Release|x86
+ {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/pythonnet.sln b/pythonnet.sln
index c5afd66c3..fdd140003 100644
--- a/pythonnet.sln
+++ b/pythonnet.sln
@@ -12,6 +12,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Console", "src\console\Cons
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "clrmodule", "src\clrmodule\clrmodule.csproj", "{86E834DE-1139-4511-96CC-69636A56E7AC}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Python.DomainReloadTests", "src\domain_tests\Python.DomainReloadTests.csproj", "{7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
DebugMono|x64 = DebugMono|x64
@@ -184,6 +186,30 @@ Global
{86E834DE-1139-4511-96CC-69636A56E7AC}.ReleaseWinPY3|x64.Build.0 = ReleaseWinPY3|x64
{86E834DE-1139-4511-96CC-69636A56E7AC}.ReleaseWinPY3|x86.ActiveCfg = ReleaseWinPY3|x86
{86E834DE-1139-4511-96CC-69636A56E7AC}.ReleaseWinPY3|x86.Build.0 = ReleaseWinPY3|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugMono|x64.ActiveCfg = Debug|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugMono|x86.ActiveCfg = Debug|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugMonoPY3|x64.ActiveCfg = Debug|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugMonoPY3|x86.ActiveCfg = Debug|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWin|x64.ActiveCfg = Debug|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWin|x64.Build.0 = Debug|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWin|x86.ActiveCfg = Debug|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWin|x86.Build.0 = Debug|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWinPY3|x64.ActiveCfg = Debug|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWinPY3|x64.Build.0 = Debug|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWinPY3|x86.ActiveCfg = Debug|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWinPY3|x86.Build.0 = Debug|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseMono|x64.ActiveCfg = Release|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseMono|x86.ActiveCfg = Release|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseMonoPY3|x64.ActiveCfg = ReleaseMon|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseMonoPY3|x86.ActiveCfg = ReleaseMon|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWin|x64.ActiveCfg = Release|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWin|x64.Build.0 = Release|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWin|x86.ActiveCfg = Release|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWin|x86.Build.0 = Release|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWinPY3|x64.ActiveCfg = Release|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWinPY3|x64.Build.0 = Release|x64
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWinPY3|x86.ActiveCfg = Release|x86
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWinPY3|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/domain_tests/App.config b/src/domain_tests/App.config
new file mode 100644
index 000000000..56efbc7b5
--- /dev/null
+++ b/src/domain_tests/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/domain_tests/Python.DomainReloadTests.15.csproj b/src/domain_tests/Python.DomainReloadTests.15.csproj
new file mode 100644
index 000000000..a6953ca64
--- /dev/null
+++ b/src/domain_tests/Python.DomainReloadTests.15.csproj
@@ -0,0 +1,66 @@
+
+
+
+
+ net40;netcoreapp3.1
+ x64;x86
+ Debug;Release
+ Exe
+ false
+ Python.DomainReloadTests
+ Python.DomainReloadTests
+ Python.DomainReloadTests
+ 2.5.0
+ false
+ false
+ false
+ false
+ bin\
+ false
+ $(OutputPath)\$(AssemblyName).xml
+ $(OutputPath)\$(TargetFramework)\$(AssemblyName).xml
+ 1591
+ ..\..\
+ $(SolutionDir)\bin\
+ $(OutputPath)\$(TargetFramework)_publish
+ 7.3
+ prompt
+ XPLAT
+ $(DefineConstants);$(BaseDefineConstants);
+ $(DefineConstants);NETCOREAPP
+ $(DefineConstants);NETSTANDARD
+ $(DefineConstants);TRACE;DEBUG
+ $(NuGetPackageRoot)\microsoft.targetingpack.netframework.v4.5\1.0.1\lib\net45\
+
+
+ x86
+
+
+ x64
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(TargetPath)
+ $(TargetDir)$(TargetName).pdb
+
+
+
diff --git a/src/domain_tests/Python.DomainReloadTests.csproj b/src/domain_tests/Python.DomainReloadTests.csproj
new file mode 100644
index 000000000..af454c89d
--- /dev/null
+++ b/src/domain_tests/Python.DomainReloadTests.csproj
@@ -0,0 +1,39 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}
+ Exe
+ Python.DomainReloadTests
+ Python.DomainReloadTests
+ v4.0
+ bin\
+ 512
+ true
+ true
+
+
+ x86
+
+
+ x64
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/domain_tests/TestRunner.cs b/src/domain_tests/TestRunner.cs
new file mode 100644
index 000000000..f8474d99b
--- /dev/null
+++ b/src/domain_tests/TestRunner.cs
@@ -0,0 +1,978 @@
+// We can't refer to or use Python.Runtime here.
+// We want it to be loaded only inside the subdomains
+using System;
+using Microsoft.CSharp;
+using System.CodeDom.Compiler;
+using System.IO;
+using System.Linq;
+
+namespace Python.DomainReloadTests
+{
+ ///
+ /// This class provides an executable that can run domain reload tests.
+ /// The setup is a bit complicated:
+ /// 1. pytest runs test_*.py in this directory.
+ /// 2. test_classname runs Python.DomainReloadTests.exe (this class) with an argument
+ /// 3. This class at runtime creates a directory that has both C# and
+ /// python code, and compiles the C#.
+ /// 4. This class then runs the C# code.
+ ///
+ /// But wait there's more indirection. The C# code that's run -- known as
+ /// the test runner --
+ /// This class compiles a DLL that contains the class which code will change
+ /// and a runner executable that will run Python code referencing the class.
+ /// Each test case:
+ /// * Compiles some code, loads it into a domain, runs python that refers to it.
+ /// * Unload the domain.
+ /// * Compile a new piece of code, load it into a domain, run a new piece of python that accesses the code.
+ /// * Unload the domain. Reload the domain, run the same python again.
+ /// This class gets built into an executable which takes one argument:
+ /// which test case to run. That's because pytest assumes we'll run
+ /// everything in one process, but we really want a clean process on each
+ /// test case to test the init/reload/teardown parts of the domain reload
+ /// code.
+ ///
+ class TestRunner
+ {
+ const string TestAssemblyName = "DomainTests";
+
+ class TestCase
+ {
+ ///
+ /// The key to pass as an argument to choose this test.
+ ///
+ public string Name;
+
+ ///
+ /// The C# code to run in the first domain.
+ ///
+ public string DotNetBefore;
+
+ ///
+ /// The C# code to run in the second domain.
+ ///
+ public string DotNetAfter;
+
+ ///
+ /// The Python code to run as a module that imports the C#.
+ /// It should have two functions: before() and after(). Before
+ /// will be called when DotNetBefore is loaded; after will be
+ /// called (twice) when DotNetAfter is loaded.
+ /// To make the test fail, have those functions raise exceptions.
+ ///
+ /// Make sure there's no leading spaces since Python cares.
+ ///
+ public string PythonCode;
+ }
+
+ static TestCase[] Cases = new TestCase[]
+ {
+ new TestCase
+ {
+ Name = "class_rename",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Before { }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class After { }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+import TestNamespace
+
+def before_reload():
+ sys.my_cls = TestNamespace.Before
+
+
+def after_reload():
+ assert sys.my_cls is not None
+ try:
+ foo = TestNamespace.Before
+ except AttributeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+ ",
+ },
+
+ new TestCase
+ {
+ Name = "static_member_rename",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls { public static int Before() { return 5; } }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls { public static int After() { return 10; } }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+import TestNamespace
+
+def before_reload():
+ sys.my_cls = TestNamespace.Cls
+ sys.my_fn = TestNamespace.Cls.Before
+ sys.my_fn()
+ TestNamespace.Cls.Before()
+
+def after_reload():
+
+ # We should have reloaded the class so we can access the new function.
+ assert 10 == sys.my_cls.After()
+ assert True is True
+
+ try:
+ # We should have reloaded the class. The old function still exists, but is now invalid.
+ sys.my_cls.Before()
+ except AttributeError:
+ print('Caught expected TypeError')
+ else:
+ raise AssertionError('Failed to throw exception: expected TypeError calling class member that no longer exists')
+
+ assert sys.my_fn is not None
+
+ try:
+ # Unbound functions still exist. They will error out when called though.
+ sys.my_fn()
+ except TypeError:
+ print('Caught expected TypeError')
+ else:
+ raise AssertionError('Failed to throw exception: expected TypeError calling unbound .NET function that no longer exists')
+ ",
+ },
+
+
+ new TestCase
+ {
+ Name = "member_rename",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls { public int Before() { return 5; } }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls { public int After() { return 10; } }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+import TestNamespace
+
+def before_reload():
+ sys.my_cls = TestNamespace.Cls()
+ sys.my_fn = TestNamespace.Cls().Before
+ sys.my_fn()
+ TestNamespace.Cls().Before()
+
+def after_reload():
+
+ # We should have reloaded the class so we can access the new function.
+ assert 10 == sys.my_cls.After()
+ assert True is True
+
+ try:
+ # We should have reloaded the class. The old function still exists, but is now invalid.
+ sys.my_cls.Before()
+ except AttributeError:
+ print('Caught expected TypeError')
+ else:
+ raise AssertionError('Failed to throw exception: expected TypeError calling class member that no longer exists')
+
+ assert sys.my_fn is not None
+
+ try:
+ # Unbound functions still exist. They will error out when called though.
+ sys.my_fn()
+ except TypeError:
+ print('Caught expected TypeError')
+ else:
+ raise AssertionError('Failed to throw exception: expected TypeError calling unbound .NET function that no longer exists')
+ ",
+ },
+
+ new TestCase
+ {
+ Name = "field_rename",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ static public int Before = 2;
+ }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ static public int After = 4;
+ }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+from TestNamespace import Cls
+
+def before_reload():
+ sys.my_int = Cls.Before
+
+def after_reload():
+ print(sys.my_int)
+ try:
+ assert 2 == Cls.Before
+ except AttributeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+",
+ },
+ new TestCase
+ {
+ Name = "property_rename",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ static public int Before { get { return 2; } }
+ }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ static public int After { get { return 4; } }
+ }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+from TestNamespace import Cls
+
+def before_reload():
+ sys.my_int = Cls.Before
+
+def after_reload():
+ print(sys.my_int)
+ try:
+ assert 2 == Cls.Before
+ except AttributeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+",
+ },
+
+ new TestCase
+ {
+ Name = "event_rename",
+ DotNetBefore = @"
+ using System;
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static event Action Before;
+ public static void Call()
+ {
+ Before();
+ }
+ }
+ }",
+ DotNetAfter = @"
+ using System;
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static event Action After;
+ public static void Call()
+ {
+ After();
+ }
+ }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+from TestNamespace import Cls
+
+called = False
+
+def callback_function():
+ global called
+ called = True
+
+def before_reload():
+ global called
+ called = False
+ Cls.Before += callback_function
+ Cls.Call()
+ assert called is True
+
+def after_reload():
+ global called
+ assert called is True
+ called = False
+ Cls.Call()
+ assert called is False
+ #try:
+ # assert 2 == Cls.Before
+ #except TypeError:
+ # print('Caught expected exception')
+ #else:
+ # raise AssertionError('Failed to throw exception')
+",
+ },
+
+ new TestCase
+ {
+ Name = "namespace_rename",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public int Foo;
+ public Cls(int i)
+ {
+ Foo = i;
+ }
+ }
+ }",
+ DotNetAfter = @"
+ namespace NewTestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public int Foo;
+ public Cls(int i)
+ {
+ Foo = i;
+ }
+ }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+import TestNamespace
+
+def before_reload():
+ sys.my_cls = TestNamespace.Cls
+ sys.my_inst = TestNamespace.Cls(1)
+
+def after_reload():
+ try:
+ TestNamespace.Cls(2)
+ sys.my_cls.Member()
+ except AttributeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+ ",
+ },
+
+ new TestCase
+ {
+ Name = "field_visibility_change",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static int Foo = 1;
+ public static int Field = 2;
+ }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static int Foo = 1;
+ private static int Field = 2;
+ }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+from TestNamespace import Cls
+
+def before_reload():
+ assert 2 == Cls.Field
+ assert 1 == Cls.Foo
+
+def after_reload():
+ assert 1 == Cls.Foo
+ try:
+ assert 1 == Cls.Field
+ except AttributeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+ ",
+ },
+
+ new TestCase
+ {
+ Name = "method_visibility_change",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static int Foo() { return 1; }
+ public static int Function() { return 2; }
+ }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static int Foo() { return 1; }
+ private static int Function() { return 2; }
+ }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+from TestNamespace import Cls
+
+def before_reload():
+ sys.my_func = Cls.Function
+ assert 1 == Cls.Foo()
+ assert 2 == Cls.Function()
+
+def after_reload():
+ assert 1 == Cls.Foo()
+ try:
+ assert 2 == Cls.Function()
+ except AttributeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+
+ try:
+ assert 2 == sys.my_func()
+ except TypeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+ ",
+ },
+
+ new TestCase
+ {
+ Name = "property_visibility_change",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static int Foo { get { return 1; } }
+ public static int Property { get { return 2; } }
+ }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static int Foo { get { return 1; } }
+ private static int Property { get { return 2; } }
+ }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+from TestNamespace import Cls
+
+def before_reload():
+ assert 1 == Cls.Foo
+ assert 2 == Cls.Property
+
+def after_reload():
+ assert 1 == Cls.Foo
+ try:
+ assert 2 == Cls.Property
+ except AttributeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+ ",
+ },
+
+ new TestCase
+ {
+ Name = "class_visibility_change",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class PublicClass { }
+
+ [System.Serializable]
+ public class Cls { }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ internal class Cls { }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+import TestNamespace
+
+def before_reload():
+ sys.my_cls = TestNamespace.Cls
+
+def after_reload():
+ sys.my_cls()
+
+ try:
+ TestNamespace.Cls()
+ except AttributeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+ ",
+ },
+
+ new TestCase
+ {
+ Name = "method_parameters_change",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static void MyFunction(int a)
+ {
+ System.Console.WriteLine(string.Format(""MyFunction says: {0}"", a));
+ }
+ }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static void MyFunction(string a)
+ {
+ System.Console.WriteLine(string.Format(""MyFunction says: {0}"", a));
+ }
+ }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+from TestNamespace import Cls
+
+def before_reload():
+ sys.my_cls = Cls
+ sys.my_func = Cls.MyFunction
+ sys.my_cls.MyFunction(1)
+ sys.my_func(2)
+
+def after_reload():
+ try:
+ sys.my_cls.MyFunction(1)
+ except TypeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+
+ try:
+ sys.my_func(2)
+ except TypeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+
+ # Calling the function from the class passes
+ sys.my_cls.MyFunction('test')
+
+ try:
+ # calling the callable directly fails
+ sys.my_func('test')
+ except TypeError:
+ print('Caught expected exception')
+ else:
+ raise AssertionError('Failed to throw exception')
+
+ Cls.MyFunction('another test')
+
+ ",
+ },
+
+ new TestCase
+ {
+ Name = "method_return_type_change",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static int MyFunction()
+ {
+ return 2;
+ }
+ }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ public static string MyFunction()
+ {
+ return ""22"";
+ }
+ }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+from TestNamespace import Cls
+
+def before_reload():
+ sys.my_cls = Cls
+ sys.my_func = Cls.MyFunction
+ assert 2 == sys.my_cls.MyFunction()
+ assert 2 == sys.my_func()
+
+def after_reload():
+ assert '22' == sys.my_cls.MyFunction()
+ assert '22' == sys.my_func()
+ assert '22' == Cls.MyFunction()
+ ",
+ },
+
+ new TestCase
+ {
+ Name = "field_type_change",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ static public int Field = 2;
+ }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Cls
+ {
+ static public string Field = ""22"";
+ }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+from TestNamespace import Cls
+
+def before_reload():
+ sys.my_cls = Cls
+ assert 2 == sys.my_cls.Field
+
+def after_reload():
+ assert '22' == Cls.Field
+ assert '22' == sys.my_cls.Field
+ ",
+ },
+
+ new TestCase
+ {
+ Name = "construct_removed_class",
+ DotNetBefore = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class Before { }
+ }",
+ DotNetAfter = @"
+ namespace TestNamespace
+ {
+ [System.Serializable]
+ public class After { }
+ }",
+ PythonCode = @"
+import clr
+import sys
+clr.AddReference('DomainTests')
+import TestNamespace
+
+def before_reload():
+ sys.my_cls = TestNamespace.Before
+
+def after_reload():
+ bar = sys.my_cls()
+
+ # Don't crash!
+ print(bar)
+ print(bar.__str__())
+ print(bar.__repr__())
+ ",
+ },
+ };
+
+ ///
+ /// The runner's code. Runs the python code
+ /// This is a template for string.Format
+ /// Arg 0 is the reload mode: ShutdownMode.Reload or other.
+ /// Arg 1 is the no-arg python function to run, before or after.
+ ///
+ const string CaseRunnerTemplate = @"
+using System;
+using System.IO;
+using Python.Runtime;
+namespace CaseRunner
+{{
+ class CaseRunner
+ {{
+ public static int Main()
+ {{
+ try
+ {{
+ PythonEngine.Initialize(mode:{0});
+ using (Py.GIL())
+ {{
+ // Because the generated assemblies are in the $TEMP folder, add it to the path
+ var temp = Path.GetTempPath();
+ dynamic sys = Py.Import(""sys"");
+ sys.path.append(new PyString(temp));
+ dynamic test_mod = Py.Import(""domain_test_module.mod"");
+ test_mod.{1}_reload();
+ }}
+ PythonEngine.Shutdown();
+ }}
+ catch (PythonException pe)
+ {{
+ throw new ArgumentException(message:pe.Message+"" ""+pe.StackTrace);
+ }}
+ catch (Exception e)
+ {{
+ Console.WriteLine(e.StackTrace);
+ throw;
+ }}
+ return 0;
+ }}
+ }}
+}}
+";
+ readonly static string PythonDllLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../runtime/bin/Python.Runtime.dll");
+
+ public static int Main(string[] args)
+ {
+// We require this slightly convoluted way of ifdef'ing because the Python
+// comments '#' are mistaken as C# preprocessior directive
+#if !NETCOREAPP
+ TestCase testCase;
+ if (args.Length < 1)
+ {
+ testCase = Cases[0];
+ }
+ else
+ {
+ string testName = args[0];
+ Console.WriteLine($"-- Looking for domain reload test case {testName}");
+ testCase = Cases.First(c => c.Name == testName);
+ }
+ Console.WriteLine($"-- Running domain reload test case: {testCase.Name}");
+
+ var tempFolderPython = Path.Combine(Path.GetTempPath(), "Python.Runtime.dll");
+ if (File.Exists(tempFolderPython))
+ {
+ File.Delete(tempFolderPython);
+ }
+
+ File.Copy(PythonDllLocation, tempFolderPython);
+
+ CreatePythonModule(testCase);
+ {
+ var runnerAssembly = CreateCaseRunnerAssembly(verb:"before");
+ CreateTestClassAssembly(testCase.DotNetBefore);
+ {
+ var runnerDomain = CreateDomain("case runner before");
+ RunAndUnload(runnerDomain, runnerAssembly);
+ }
+ {
+ var runnerDomain = CreateDomain("case runner before (again)");
+ RunAndUnload(runnerDomain, runnerAssembly);
+ }
+ }
+
+ {
+ var runnerAssembly = CreateCaseRunnerAssembly(verb:"after");
+ CreateTestClassAssembly(testCase.DotNetAfter);
+
+ // Do it twice for good measure
+ {
+ var runnerDomain = CreateDomain("case runner after");
+ RunAndUnload(runnerDomain, runnerAssembly);
+ }
+ {
+ var runnerDomain = CreateDomain("case runner after (again)");
+ RunAndUnload(runnerDomain, runnerAssembly);
+ }
+ }
+#endif
+ return 0;
+ }
+#if !NETCOREAPP
+
+ static void RunAndUnload(AppDomain domain, string assemblyPath)
+ {
+ // Somehow the stack traces during execution sometimes have the wrong line numbers.
+ // Add some info for when debugging is required.
+ Console.WriteLine($"-- Running domain {domain.FriendlyName}");
+ domain.ExecuteAssembly(assemblyPath);
+ AppDomain.Unload(domain);
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ }
+
+ static string CreateTestClassAssembly(string code)
+ {
+ return CreateAssembly(TestAssemblyName + ".dll", code, exe: false);
+ }
+
+ static string CreateCaseRunnerAssembly(string verb, string shutdownMode = "ShutdownMode.Reload")
+ {
+ var code = string.Format(CaseRunnerTemplate, shutdownMode, verb);
+ var name = "TestCaseRunner.exe";
+
+ return CreateAssembly(name, code, exe: true);
+ }
+ static string CreateAssembly(string name, string code, bool exe = false)
+ {
+ // Console.WriteLine(code);
+ // Never return or hold the Assembly instance. This will cause
+ // the assembly to be loaded into the current domain and this
+ // interferes with the tests. The Domain can execute fine from a
+ // path, so let's return that.
+ CSharpCodeProvider provider = new CSharpCodeProvider();
+ CompilerParameters parameters = new CompilerParameters();
+ parameters.GenerateExecutable = exe;
+ var assemblyName = name;
+ var assemblyFullPath = Path.Combine(Path.GetTempPath(), assemblyName);
+ parameters.OutputAssembly = assemblyFullPath;
+ parameters.ReferencedAssemblies.Add("System.dll");
+ parameters.ReferencedAssemblies.Add("System.Core.dll");
+ parameters.ReferencedAssemblies.Add("Microsoft.CSharp.dll");
+ parameters.ReferencedAssemblies.Add(PythonDllLocation);
+ CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
+ if (results.NativeCompilerReturnValue != 0)
+ {
+ var stderr = System.Console.Error;
+ stderr.WriteLine($"Error in {name} compiling:\n{code}");
+ foreach (var error in results.Errors)
+ {
+ stderr.WriteLine(error);
+ }
+ throw new ArgumentException("Error compiling code");
+ }
+
+ return assemblyFullPath;
+ }
+
+ static AppDomain CreateDomain(string name)
+ {
+ // Create the domain. Make sure to set PrivateBinPath to a relative
+ // path from the CWD (namely, 'bin').
+ // See https://stackoverflow.com/questions/24760543/createinstanceandunwrap-in-another-domain
+ var currentDomain = AppDomain.CurrentDomain;
+ var domainsetup = new AppDomainSetup()
+ {
+ ApplicationBase = Path.GetTempPath(),
+ ConfigurationFile = currentDomain.SetupInformation.ConfigurationFile,
+ LoaderOptimization = LoaderOptimization.SingleDomain,
+ PrivateBinPath = "."
+ };
+ var domain = AppDomain.CreateDomain(
+ $"My Domain {name}",
+ currentDomain.Evidence,
+ domainsetup);
+
+ return domain;
+ }
+
+ static string CreatePythonModule(TestCase testCase)
+ {
+ var modulePath = Path.Combine(Path.GetTempPath(), "domain_test_module");
+ if (Directory.Exists(modulePath))
+ {
+ Directory.Delete(modulePath, recursive: true);
+ }
+ Directory.CreateDirectory(modulePath);
+
+ File.Create(Path.Combine(modulePath, "__init__.py")).Close(); //Create and don't forget to close!
+ using (var writer = File.CreateText(Path.Combine(modulePath, "mod.py")))
+ {
+ writer.Write(testCase.PythonCode);
+ }
+
+ return null;
+ }
+#endif
+
+ }
+}
diff --git a/src/domain_tests/test_domain_reload.py b/src/domain_tests/test_domain_reload.py
new file mode 100644
index 000000000..e4d6bf6d7
--- /dev/null
+++ b/src/domain_tests/test_domain_reload.py
@@ -0,0 +1,73 @@
+import subprocess
+import os
+
+import pytest
+
+def _run_test(testname):
+ dirname = os.path.split(__file__)[0]
+ exename = os.path.join(dirname, 'bin', 'Python.DomainReloadTests.exe')
+ proc = subprocess.Popen([
+ exename,
+ testname,
+ ])
+ proc.wait()
+
+ assert proc.returncode == 0
+
+@pytest.mark.xfail(reason="Issue not yet fixed.")
+def test_rename_class():
+ _run_test('class_rename')
+
+@pytest.mark.xfail(reason="Issue not yet fixed.")
+def test_rename_class_member_static_function():
+ _run_test('static_member_rename')
+
+@pytest.mark.xfail(reason="Issue not yet fixed.")
+def test_rename_class_member_function():
+ _run_test('member_rename')
+
+@pytest.mark.xfail(reason="Issue not yet fixed.")
+def test_rename_class_member_field():
+ _run_test('field_rename')
+
+@pytest.mark.xfail(reason="Issue not yet fixed.")
+def test_rename_class_member_property():
+ _run_test('property_rename')
+
+@pytest.mark.xfail(reason="Issue not yet fixed.")
+def test_rename_namespace():
+ _run_test('namespace_rename')
+
+@pytest.mark.xfail(reason="Issue not yet fixed.")
+def test_field_visibility_change():
+ _run_test("field_visibility_change")
+
+@pytest.mark.xfail(reason="Issue not yet fixed.")
+def test_method_visibility_change():
+ _run_test("method_visibility_change")
+
+@pytest.mark.xfail(reason="Issue not yet fixed.")
+def test_property_visibility_change():
+ _run_test("property_visibility_change")
+
+@pytest.mark.xfail(reason="Issue not yet fixed.")
+def test_class_visibility_change():
+ _run_test("class_visibility_change")
+
+@pytest.mark.xfail(reason="Issue not yet fixed.")
+def test_method_parameters_change():
+ _run_test("method_parameters_change")
+
+def test_method_return_type_change():
+ _run_test("method_return_type_change")
+
+def test_field_type_change():
+ _run_test("field_type_change")
+
+@pytest.mark.xfail(reason="Events not yet serializable")
+def test_rename_event():
+ _run_test('event_rename')
+
+@pytest.mark.xfail(reason="newly instanced object uses PyType_GenericAlloc")
+def test_construct_removed_class():
+ _run_test("construct_removed_class")
\ No newline at end of file