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