diff --git a/pythonnet.15.sln b/pythonnet.15.sln index ce863817f..cfe3807ae 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Python.DomainReloadTests", "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,66 @@ 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|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x64.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Debug|x86.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|Any CPU.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|Any CPU.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|x64.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|x64.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|x86.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMono|x86.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|Any CPU.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|Any CPU.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|x64.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|x64.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|x86.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugMonoPY3|x86.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|Any CPU.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|Any CPU.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|x64.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|x64.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|x86.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWin|x86.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|Any CPU.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|Any CPU.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|x64.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|x64.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|x86.ActiveCfg = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.DebugWinPY3|x86.Build.0 = Debug|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|Any CPU.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x64.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x64.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x86.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.Release|x86.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|Any CPU.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|Any CPU.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|x64.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|x64.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|x86.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMono|x86.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|Any CPU.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|Any CPU.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|x64.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|x64.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|x86.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseMonoPY3|x86.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|Any CPU.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|Any CPU.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|x64.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|x64.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|x86.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWin|x86.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|Any CPU.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|Any CPU.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|x64.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|x64.Build.0 = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|x86.ActiveCfg = Release|Any CPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5}.ReleaseWinPY3|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/pythonnet.sln b/pythonnet.sln index c5afd66c3..7b198b336 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 = DebugMono|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugMono|x86.ActiveCfg = DebugMono|x86 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugMonoPY3|x64.ActiveCfg = DebugMonoPY3|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugMonoPY3|x86.ActiveCfg = DebugMonoPY3|x86 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWin|x64.ActiveCfg = DebugWin|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWin|x64.Build.0 = DebugWin|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWin|x86.ActiveCfg = DebugWin|x86 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWin|x86.Build.0 = DebugWin|x86 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWinPY3|x64.ActiveCfg = DebugWinPY3|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWinPY3|x64.Build.0 = DebugWinPY3|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWinPY3|x86.ActiveCfg = DebugWinPY3|x86 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.DebugWinPY3|x86.Build.0 = DebugWinPY3|x86 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseMono|x64.ActiveCfg = ReleaseMono|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseMono|x86.ActiveCfg = ReleaseMono|x86 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseMonoPY3|x64.ActiveCfg = ReleaseMonoPY3|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseMonoPY3|x86.ActiveCfg = ReleaseMonoPY3|x86 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWin|x64.ActiveCfg = ReleaseWin|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWin|x64.Build.0 = ReleaseWin|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWin|x86.ActiveCfg = ReleaseWin|x86 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWin|x86.Build.0 = ReleaseWin|x86 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWinPY3|x64.ActiveCfg = ReleaseWinPY3|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWinPY3|x64.Build.0 = ReleaseWinPY3|x64 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWinPY3|x86.ActiveCfg = ReleaseWinPY3|x86 + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2}.ReleaseWinPY3|x86.Build.0 = ReleaseWinPY3|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..0b69ccb3d --- /dev/null +++ b/src/domain_tests/Python.DomainReloadTests.15.csproj @@ -0,0 +1,53 @@ + + + + + Debug + AnyCPU + {F2FB6DA3-318E-4F30-9A1F-932C667E38C5} + Exe + Python.DomainReloadTests + Python.DomainReloadTests + bin\ + v4.0 + 512 + true + true + + + AnyCPU + true + full + false + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/domain_tests/Python.DomainReloadTests.csproj b/src/domain_tests/Python.DomainReloadTests.csproj new file mode 100644 index 000000000..0914070ce --- /dev/null +++ b/src/domain_tests/Python.DomainReloadTests.csproj @@ -0,0 +1,86 @@ + + + + + Debug + AnyCPU + {7539EBD5-7A15-4513-BCB0-1D9BEF112BC2} + Exe + Python.DomainReloadTests + Python.DomainReloadTests + v4.0 + bin\ + 512 + true + true + + + x86 + + + x64 + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + + + + + + + + + + + + + + + + + + + \ 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..33c203ba0 --- /dev/null +++ b/src/domain_tests/TestRunner.cs @@ -0,0 +1,232 @@ +// 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; + +namespace Python.DomainReloadTests +{ + /// + /// 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. + /// It's Main() will: + /// * Run the runner and unlod it's domain + /// * Modify and re-compile the test class + /// * Re-run the runner and unload it twice + /// + class TestRunner + { + /// + /// The code of the test class that changes + /// + const string ChangingClassTemplate = @" +using System; + +namespace TestNamespace +{ + [Serializable] + public class {class} + { + } +}"; + + /// + /// The Python code that accesses the test class + /// + const string PythonCode = @"import clr +clr.AddReference('TestClass') +import sys +from TestNamespace import {class} +import TestNamespace +foo = None +def do_work(): + sys.my_obj = {class} + +def test_work(): + print({class}) + print(sys.my_obj) +"; + + /// + /// The runner's code. Runs the python code + /// + 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}_work(); + }} + PythonEngine.Shutdown(); + }} + catch (PythonException pe) + {{ + throw new ArgumentException(message:pe.Message+"" ""+pe.StackTrace); + }} + return 0; + }} + }} +}} +"; + readonly static string PythonDllLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../runtime/bin/Python.Runtime.dll"); + + public static int Main(string[] args) + { + if (args.Length < 1) + { + args = new string[] {"TestClass", "NewTestClass"}; + // return 123; + } + Console.WriteLine($"Testing with arguments: {string.Join(", ", args)}"); + + var tempFolderPython = Path.Combine(Path.GetTempPath(), "Python.Runtime.dll"); + if (File.Exists(tempFolderPython)) + { + File.Delete(tempFolderPython); + } + + File.Copy(PythonDllLocation, tempFolderPython); + + CreatePythonModule(args[0]); + { + var runnerAssembly = CreateCaseRunnerAssembly(verb:"do"); + CreateTestClassAssembly(className: args[0]); + + var runnerDomain = CreateDomain("case runner"); + RunAndUnload(runnerDomain, runnerAssembly); + } + + { + var runnerAssembly = CreateCaseRunnerAssembly(verb:"test"); + // remove the method + CreateTestClassAssembly(className: args[1]); + + // Do it twice for good measure + { + var runnerDomain = CreateDomain("case runner 2"); + RunAndUnload(runnerDomain, runnerAssembly); + } + { + var runnerDomain = CreateDomain("case runner 3"); + RunAndUnload(runnerDomain, runnerAssembly); + } + } + + return 0; + } + + 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($"Runining domain {domain.FriendlyName}"); + domain.ExecuteAssembly(assemblyPath); + AppDomain.Unload(domain); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + + static string CreateTestClassAssembly(string className) + { + var name = "TestClass.dll"; + string code = ChangingClassTemplate.Replace("{class}", className); + + return CreateAssembly(name, code, exe: false); + } + + static string CreateCaseRunnerAssembly(string shutdownMode = "ShutdownMode.Reload", string verb = "do") + { + 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) + { + foreach (var error in results.Errors) + { + System.Console.WriteLine(error); + } + throw new ArgumentException(); + } + + 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(string className) + { + 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(PythonCode.Replace("{class}", className)); + } + + return null; + } + + } +} diff --git a/src/domain_tests/test_domain_reload.py b/src/domain_tests/test_domain_reload.py new file mode 100644 index 000000000..991ae7a14 --- /dev/null +++ b/src/domain_tests/test_domain_reload.py @@ -0,0 +1,22 @@ +import subprocess +import os + +def runit(m1, m2, member): + proc = subprocess.Popen([os.path.join(os.path.split(__file__)[0], 'bin', 'Python.DomainReloadTests.exe'), m1, m2, member]) + proc.wait() + + assert proc.returncode == 0 + +def test_remove_method(): + + m1 = 'public static void TestMethod() {Console.WriteLine("from test method");}' + m2 = 'public static void TestMethod2() {Console.WriteLine("from test method");}' + member = 'TestMethod' + runit(m1, m2, member) + +def test_remove_member(): + + m1 = 'public static int TestMember = -1;' + m2 = 'public static int TestMember2 = -1;' + member = 'TestMember' + runit(m1, m2, member) \ No newline at end of file diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 08dc1d860..484cc48c4 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -116,6 +116,7 @@ + diff --git a/src/runtime/fieldobject.cs b/src/runtime/fieldobject.cs index 86b93dd1b..6820927c2 100644 --- a/src/runtime/fieldobject.cs +++ b/src/runtime/fieldobject.cs @@ -1,5 +1,8 @@ using System; using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using System.IO; namespace Python.Runtime { @@ -9,11 +12,11 @@ namespace Python.Runtime [Serializable] internal class FieldObject : ExtensionType { - private FieldInfo info; + private MaybeSerialize m_info; public FieldObject(FieldInfo info) { - this.info = info; + m_info = new MaybeSerialize(info); } /// @@ -24,14 +27,25 @@ public FieldObject(FieldInfo info) public static IntPtr tp_descr_get(IntPtr ds, IntPtr ob, IntPtr tp) { var self = (FieldObject)GetManagedObject(ds); - object result; if (self == null) { return IntPtr.Zero; } + try + { + return self.tp_descr_get(ob, tp); + } + catch (Exception e) + { + Exceptions.SetError(Exceptions.TypeError, e.Message); + return IntPtr.Zero; + } + } - FieldInfo info = self.info; + IntPtr tp_descr_get(IntPtr ob, IntPtr tp) + { + FieldInfo info = m_info.Value; if (ob == IntPtr.Zero || ob == Runtime.PyNone) { @@ -43,7 +57,7 @@ public static IntPtr tp_descr_get(IntPtr ds, IntPtr ob, IntPtr tp) } try { - result = info.GetValue(null); + var result = info.GetValue(null); return Converter.ToPython(result, info.FieldType); } catch (Exception e) @@ -61,7 +75,7 @@ public static IntPtr tp_descr_get(IntPtr ds, IntPtr ob, IntPtr tp) Exceptions.SetError(Exceptions.TypeError, "instance is not a clr object"); return IntPtr.Zero; } - result = info.GetValue(co.inst); + var result = info.GetValue(co.inst); return Converter.ToPython(result, info.FieldType); } catch (Exception e) @@ -79,7 +93,6 @@ public static IntPtr tp_descr_get(IntPtr ds, IntPtr ob, IntPtr tp) public new static int tp_descr_set(IntPtr ds, IntPtr ob, IntPtr val) { var self = (FieldObject)GetManagedObject(ds); - object newval; if (self == null) { @@ -91,8 +104,21 @@ public static IntPtr tp_descr_get(IntPtr ds, IntPtr ob, IntPtr tp) Exceptions.SetError(Exceptions.TypeError, "cannot delete field"); return -1; } + try + { + return self.tp_descr_set(ob, val); + } + catch (Exception e) + { + Exceptions.SetError(Exceptions.TypeError, e.Message); + return -1; + } + } - FieldInfo info = self.info; + + int tp_descr_set(IntPtr ob, IntPtr val) + { + FieldInfo info = m_info.Value; if (info.IsLiteral || info.IsInitOnly) { @@ -111,6 +137,7 @@ public static IntPtr tp_descr_get(IntPtr ds, IntPtr ob, IntPtr tp) } } + object newval; if (!Converter.ToManaged(val, info.FieldType, out newval, true)) { return -1; @@ -147,7 +174,7 @@ public static IntPtr tp_descr_get(IntPtr ds, IntPtr ob, IntPtr tp) public static IntPtr tp_repr(IntPtr ob) { var self = (FieldObject)GetManagedObject(ob); - return Runtime.PyString_FromString($""); + return Runtime.PyString_FromString($""); } } } diff --git a/src/runtime/maybeserialize.cs b/src/runtime/maybeserialize.cs new file mode 100644 index 000000000..70733476a --- /dev/null +++ b/src/runtime/maybeserialize.cs @@ -0,0 +1,132 @@ +using System; +using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; +using System.IO; + +namespace Python.Runtime +{ + /// + /// A MaybeSerialize<T> delays errors from serialization and + /// deserialization until the item is used. + /// + /// Python for .NET uses this in the C# reloading architecture. + /// If e.g. a class member was renamed when reloading, references to the + /// old field will be invalid, but the rest of the system will still work. + /// Code that tries to use the old field will receive an exception. + /// + /// Assumption: the item being wrapped by MaybeSerialize will never be null. + /// + [Serializable] + internal struct MaybeSerialize : ISerializable where T : class + { + /// + /// The item being wrapped. + /// + /// If this is null, that means we failed to serialize or deserialize it. + /// + private T m_item; + + /// + /// A string useful for debugging the error. + /// + /// This is null if m_item deserialized properly. + /// Otherwise, it will be derived off of m_item.ToString() when we + /// serialized. + /// + private string m_name; + + /// + /// Store an item in such a way that it can be deserialized. + /// + /// It must not be null. + /// + public MaybeSerialize(T item) + { + if (item == null) + { + throw new System.ArgumentNullException("Trying to store a null"); + } + m_item = item; + m_name = null; + } + + /// + /// Get the underlying deserialized value, or throw an exception + /// if deserialiation failed. + /// + public T Value + { + get + { + if (m_item == null) + { + throw new SerializationException($"The .NET object underlying {m_name} no longer exists"); + } + return m_item; + } + } + + /// + /// Get a printable name. + /// + public string ToString() + { + if (m_item == null) + { + return $"(missing {m_name})"; + } + else + { + return m_item.ToString(); + } + } + + /// + /// Implements ISerializable + /// + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (m_item == null) + { + // Save the name; this failed to reload in a previous + // generation but we still need to remember what it was. + info.AddValue("n", m_name); + } + else + { + // Try to save the item. If it fails, too bad. + try + { + info.AddValue("i", m_item); + } + catch(SerializationException _) + { + } + + // Also save the name in case the item doesn't deserialize + info.AddValue("n", m_item.ToString()); + } + } + + /// + /// Implements ISerializable + /// + private MaybeSerialize(SerializationInfo info, StreamingContext context) + { + try + { + // Try to deserialize the item. It might fail, or it might + // have already failed so there just isn't an "i" to find. + m_item = (T)info.GetValue("i", typeof(T)); + m_name = null; + } + catch (SerializationException _) + { + // Getting the item failed, so get the name. + m_item = null; + m_name = info.GetString("n"); + } + } + } +}