From efb5be6def81d9699cc7176ee4f716ebbd24b7c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Bourbonnais?= Date: Mon, 19 Oct 2020 16:15:22 -0400 Subject: [PATCH 1/7] (WIP) Add a test framework for domain reload test cases Domain reload tests should be isolated from each other as much as possible. This framework dynamically creates sub-runners, assemblies, and python modules to do the domain load-unload in an isolated way. As each test requires it's own subprocess, we cannot use nUnit. Leverage pytest to run each subprocess instead. --- pythonnet.15.sln | 62 +++++ pythonnet.sln | 26 ++ src/domain_tests/App.config | 6 + .../Python.DomainReloadTests.15.csproj | 53 ++++ .../Python.DomainReloadTests.csproj | 86 +++++++ src/domain_tests/TestRunner.cs | 228 ++++++++++++++++++ src/domain_tests/test_domain_reload.py | 22 ++ 7 files changed, 483 insertions(+) create mode 100644 src/domain_tests/App.config create mode 100644 src/domain_tests/Python.DomainReloadTests.15.csproj create mode 100644 src/domain_tests/Python.DomainReloadTests.csproj create mode 100644 src/domain_tests/TestRunner.cs create mode 100644 src/domain_tests/test_domain_reload.py 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..261b7f20f --- /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..6699d1b26 --- /dev/null +++ b/src/domain_tests/TestRunner.cs @@ -0,0 +1,228 @@ +// 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 + /// * Run the runner and unload it's twice more + /// + class TestRunner + { + /// + /// The code of the test class that changes + /// + const string ChangingClassTemplate = @" +using System; + +namespace TestNamespace +{{ + [Serializable] + public class TestClass + {{ + {0} + {1} + }} +}}"; + + /// + /// The Python code that accesses the test class + /// + const string PythonCodeStep = @"import clr +clr.AddReference('TestClass') +import sys +from TestNamespace import TestClass +foo = None +def do_work(): + global foo + obj = TestClass() + foo = TestClass.{0} + sys.my_obj = foo + 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() + {{ + PythonEngine.Initialize(mode:{0}); + try + {{ + 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.do_work(); + }} + PythonEngine.Shutdown(); + }} + catch (PythonException pe) + {{ + throw new ArgumentException(message:pe.Message); + }} + return 0; + }} + }} +}} +"; + readonly static string PythonDllLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../runtime/bin/Python.Runtime.dll"); + + public static int Main(string[] args) + { + Console.WriteLine($"Testing with arguments: {string.Join(", ", args)}"); + if (args.Length < 3) + { + return 123; + } + var tempFolderPython = Path.Combine(Path.GetTempPath(), "Python.Runtime.dll"); + if (File.Exists(tempFolderPython)) + { + File.Delete(tempFolderPython); + } + + File.Copy(PythonDllLocation, tempFolderPython); + + CreatePythonModule(args[2]); + var runnerAssembly = CreateCaseRunnerAssembly(); + { + CreateTestClassAssembly(m1: args[0]); + + var runnerDomain = CreateDomain("case runner"); + RunAndUnload(runnerDomain, runnerAssembly); + } + + { + // remove the method + CreateTestClassAssembly(m1: 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 m1 = "", string m2 = "") + { + var name = "TestClass.dll"; + return CreateAssembly(name, string.Format(ChangingClassTemplate, m1, m2), exe: false); + } + + static string CreateCaseRunnerAssembly(string shutdownMode = "ShutdownMode.Reload") + { + var code = string.Format(CaseRunnerTemplate, shutdownMode); + var name = "TestCaseRunner.exe"; + return CreateAssembly(name, code, exe: true); + } + + static string CreateAssembly(string name, string code, bool exe = false) + { + // 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 code) + { + 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(string.Format(PythonCodeStep, code)); + } + + 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..3b3727dbd --- /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 = '' + member = 'TestMethod' + runit(m1, m2, member) + +def test_remove_member(): + + m1 = 'public static int TestMember = -1;' + m2 = '' + member = 'TestMember' + runit(m1, m2, member) \ No newline at end of file From 1f4205c9ef2bd0f8772a7830d8beffb1befd90f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Bourbonnais?= Date: Tue, 20 Oct 2020 11:39:50 -0400 Subject: [PATCH 2/7] Add a second step that makes more sense Just retrieve the object and print it. --- src/domain_tests/TestRunner.cs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/domain_tests/TestRunner.cs b/src/domain_tests/TestRunner.cs index 6699d1b26..90bfb5cc5 100644 --- a/src/domain_tests/TestRunner.cs +++ b/src/domain_tests/TestRunner.cs @@ -13,7 +13,7 @@ namespace Python.DomainReloadTests /// It's Main() will: /// * Run the runner and unlod it's domain /// * Modify and re-compile the test class - /// * Run the runner and unload it's twice more + /// * Re-run the runner and unload it twice /// class TestRunner { @@ -34,9 +34,9 @@ public class TestClass }}"; /// - /// The Python code that accesses the test class + /// The Python code that accesses the test class in the first step of the run /// - const string PythonCodeStep = @"import clr + const string PythonCodeStep1 = @"import clr clr.AddReference('TestClass') import sys from TestNamespace import TestClass @@ -49,6 +49,20 @@ global foo print(sys.my_obj) "; + /// + /// The Python code that accesses the test class + /// + const string PythonCodeStep2 = @"import clr +clr.AddReference('TestClass') +import sys +from TestNamespace import TestClass +foo = None +def do_work(): + global foo + print(foo) + print(sys.my_obj) +"; + /// /// The runner's code. Runs the python code /// @@ -102,7 +116,7 @@ public static int Main(string[] args) File.Copy(PythonDllLocation, tempFolderPython); - CreatePythonModule(args[2]); + CreatePythonModule(string.Format(PythonCodeStep1, args[2])); var runnerAssembly = CreateCaseRunnerAssembly(); { CreateTestClassAssembly(m1: args[0]); @@ -111,6 +125,9 @@ public static int Main(string[] args) RunAndUnload(runnerDomain, runnerAssembly); } + // Re-create the python module to checkup on the members + CreatePythonModule(PythonCodeStep2); + { // remove the method CreateTestClassAssembly(m1: args[1]); @@ -165,10 +182,7 @@ static string CreateAssembly(string name, string code, bool exe = false) 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"); @@ -218,7 +232,7 @@ static string CreatePythonModule(string code) 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(string.Format(PythonCodeStep, code)); + writer.Write(code); } return null; From 7d6e6e9c48fad1264e3f231b63d58448276f232c Mon Sep 17 00:00:00 2001 From: benoithudson Date: Fri, 9 Oct 2020 13:29:00 -0400 Subject: [PATCH 3/7] Bug 1250: fix for missing FieldInfo This fixes the error in bug #1250 by separately serializing the FieldInfo. Probably this can be optimized a bunch. I haven't verified the performance implications at all. --- src/runtime/fieldobject.cs | 131 ++++++++++++++++++++++++++++++++++--- 1 file changed, 122 insertions(+), 9 deletions(-) diff --git a/src/runtime/fieldobject.cs b/src/runtime/fieldobject.cs index 86b93dd1b..35ea002ee 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,97 @@ namespace Python.Runtime [Serializable] internal class FieldObject : ExtensionType { - private FieldInfo info; + [Serializable] + private struct SerializedFieldInfo : ISerializable + { + // The field if we can find it. Otherwise null. + private FieldInfo m_info; + + // The name of the field if the field is missing. Otherwise null. + private string m_name; + + public SerializedFieldInfo(FieldInfo info) + { + if (info == null) + { + throw new System.ArgumentNullException("null FieldInfo"); + } + m_info = info; + m_name = null; + } + + public FieldInfo Value + { + get + { + if (m_info == null) + { + throw new SerializationException($".NET field {m_name} was renamed or removed during domain reload"); + } + return m_info; + } + } + + public string Name + { + get + { + if (m_info == null) + { + return $"(missing {m_name})"; + } + else + { + return m_info.Name; + } + } + } + + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (m_info == null) + { + info.AddValue("n", m_name); + } + else + { + // Serialize in a silly way. TODO optimize. + var formatter = new BinaryFormatter(); + using (var ms = new MemoryStream()) + { + formatter.Serialize(ms, m_info); + info.AddValue("i", ms.ToArray()); + } + + // Also save the name in case the info doesn't deserialize + info.AddValue("n", m_info.ToString()); + } + } + + private SerializedFieldInfo(SerializationInfo info, StreamingContext context) + { + try + { + var serialized = (byte[])info.GetValue("i", typeof(byte[])); + var formatter = new BinaryFormatter(); + using (var ms = new MemoryStream(serialized)) + { + m_info = (FieldInfo)formatter.Deserialize(ms); + } + } + catch (SerializationException _) + { + m_info = null; + } + m_name = (m_info != null) ? null : info.GetString("n"); + } + } + + private SerializedFieldInfo m_info; public FieldObject(FieldInfo info) { - this.info = info; + m_info = new SerializedFieldInfo(info); } /// @@ -24,14 +113,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 +143,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 +161,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 +179,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 +190,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 +223,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 +260,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($""); } } } From 9591cec7e6b4cdfa52ed7c35b8ce7687acecf38c Mon Sep 17 00:00:00 2001 From: benoithudson Date: Sun, 18 Oct 2020 23:51:27 -0400 Subject: [PATCH 4/7] Bug 1250: support for easier serialization Adds MaybeSerialize which handles the reload nicely. Shows how to use it in FieldObject; it's very minimal changes to FieldObject to add this support. This is simpler than the proof of concept code. --- src/runtime/Python.Runtime.csproj | 1 + src/runtime/fieldobject.cs | 92 +-------------------- src/runtime/maybeserialize.cs | 132 ++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 89 deletions(-) create mode 100644 src/runtime/maybeserialize.cs 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 35ea002ee..6820927c2 100644 --- a/src/runtime/fieldobject.cs +++ b/src/runtime/fieldobject.cs @@ -12,97 +12,11 @@ namespace Python.Runtime [Serializable] internal class FieldObject : ExtensionType { - [Serializable] - private struct SerializedFieldInfo : ISerializable - { - // The field if we can find it. Otherwise null. - private FieldInfo m_info; - - // The name of the field if the field is missing. Otherwise null. - private string m_name; - - public SerializedFieldInfo(FieldInfo info) - { - if (info == null) - { - throw new System.ArgumentNullException("null FieldInfo"); - } - m_info = info; - m_name = null; - } - - public FieldInfo Value - { - get - { - if (m_info == null) - { - throw new SerializationException($".NET field {m_name} was renamed or removed during domain reload"); - } - return m_info; - } - } - - public string Name - { - get - { - if (m_info == null) - { - return $"(missing {m_name})"; - } - else - { - return m_info.Name; - } - } - } - - public void GetObjectData(SerializationInfo info, StreamingContext context) - { - if (m_info == null) - { - info.AddValue("n", m_name); - } - else - { - // Serialize in a silly way. TODO optimize. - var formatter = new BinaryFormatter(); - using (var ms = new MemoryStream()) - { - formatter.Serialize(ms, m_info); - info.AddValue("i", ms.ToArray()); - } - - // Also save the name in case the info doesn't deserialize - info.AddValue("n", m_info.ToString()); - } - } - - private SerializedFieldInfo(SerializationInfo info, StreamingContext context) - { - try - { - var serialized = (byte[])info.GetValue("i", typeof(byte[])); - var formatter = new BinaryFormatter(); - using (var ms = new MemoryStream(serialized)) - { - m_info = (FieldInfo)formatter.Deserialize(ms); - } - } - catch (SerializationException _) - { - m_info = null; - } - m_name = (m_info != null) ? null : info.GetString("n"); - } - } - - private SerializedFieldInfo m_info; + private MaybeSerialize m_info; public FieldObject(FieldInfo info) { - m_info = new SerializedFieldInfo(info); + m_info = new MaybeSerialize(info); } /// @@ -260,7 +174,7 @@ int tp_descr_set(IntPtr ob, IntPtr val) 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"); + } + } + } +} From 005a476e7bb826dba154f2f80b3d1ca7d3de9f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Bourbonnais?= Date: Thu, 22 Oct 2020 13:32:09 -0400 Subject: [PATCH 5/7] (wip) --- src/domain_tests/Python.DomainReloadTests.15.csproj | 2 +- src/domain_tests/TestRunner.cs | 9 ++++++--- src/domain_tests/test_domain_reload.py | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/domain_tests/Python.DomainReloadTests.15.csproj b/src/domain_tests/Python.DomainReloadTests.15.csproj index 261b7f20f..0b69ccb3d 100644 --- a/src/domain_tests/Python.DomainReloadTests.15.csproj +++ b/src/domain_tests/Python.DomainReloadTests.15.csproj @@ -1,4 +1,4 @@ - + diff --git a/src/domain_tests/TestRunner.cs b/src/domain_tests/TestRunner.cs index 90bfb5cc5..ca9b0e938 100644 --- a/src/domain_tests/TestRunner.cs +++ b/src/domain_tests/TestRunner.cs @@ -92,7 +92,7 @@ public static int Main() }} catch (PythonException pe) {{ - throw new ArgumentException(message:pe.Message); + throw new ArgumentException(message:pe.Message+"" ""+pe.StackTrace); }} return 0; }} @@ -103,11 +103,13 @@ public static int Main() public static int Main(string[] args) { - Console.WriteLine($"Testing with arguments: {string.Join(", ", args)}"); if (args.Length < 3) { - return 123; + args = new string[] { "public static int TestMember = 2;", "", "TestMember" }; + // return 123; } + Console.WriteLine($"Testing with arguments: {string.Join(", ", args)}"); + var tempFolderPython = Path.Combine(Path.GetTempPath(), "Python.Runtime.dll"); if (File.Exists(tempFolderPython)) { @@ -173,6 +175,7 @@ static string CreateCaseRunnerAssembly(string shutdownMode = "ShutdownMode.Reloa 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 diff --git a/src/domain_tests/test_domain_reload.py b/src/domain_tests/test_domain_reload.py index 3b3727dbd..991ae7a14 100644 --- a/src/domain_tests/test_domain_reload.py +++ b/src/domain_tests/test_domain_reload.py @@ -10,13 +10,13 @@ def runit(m1, m2, member): def test_remove_method(): m1 = 'public static void TestMethod() {Console.WriteLine("from test method");}' - m2 = '' + 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 = '' + m2 = 'public static int TestMember2 = -1;' member = 'TestMember' runit(m1, m2, member) \ No newline at end of file From a5cf8a4443c8ddd738304b1aa0e53aeb7a427049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Bourbonnais?= Date: Tue, 27 Oct 2020 11:35:58 -0400 Subject: [PATCH 6/7] (wip) --- src/domain_tests/TestRunner.cs | 78 +++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/src/domain_tests/TestRunner.cs b/src/domain_tests/TestRunner.cs index ca9b0e938..169cea56b 100644 --- a/src/domain_tests/TestRunner.cs +++ b/src/domain_tests/TestRunner.cs @@ -33,34 +33,39 @@ public class TestClass }} }}"; - /// - /// The Python code that accesses the test class in the first step of the run - /// - const string PythonCodeStep1 = @"import clr -clr.AddReference('TestClass') -import sys -from TestNamespace import TestClass -foo = None -def do_work(): - global foo - obj = TestClass() - foo = TestClass.{0} - sys.my_obj = foo - print(sys.my_obj) -"; +// /// +// /// The Python code that accesses the test class in the first step of the run +// /// +// const string PythonCodeStep1 = @"import clr +// clr.AddReference('TestClass') +// import sys +// from TestNamespace import TestClass +// foo = None +// def do_work(): +// global foo +// obj = TestClass() +// foo = TestClass.{0} +// sys.my_obj = foo +// print(sys.my_obj) +// "; /// /// The Python code that accesses the test class /// - const string PythonCodeStep2 = @"import clr + const string PythonCodeStep = @"import clr clr.AddReference('TestClass') import sys from TestNamespace import TestClass +import TestNamespace foo = None def do_work(): - global foo + obj = TestClass() + foo = TestClass.{0} + sys.my_obj = foo + print(sys.my_obj) + +def test_work(): print(foo) - print(sys.my_obj) "; /// @@ -76,18 +81,19 @@ class CaseRunner {{ public static int Main() {{ - PythonEngine.Initialize(mode:{0}); try {{ - 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.do_work(); - }} + 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""); + // // Console.WriteLine(""verb: {1}""); + // // test_mod.{1}_work(); + // }} PythonEngine.Shutdown(); }} catch (PythonException pe) @@ -105,7 +111,8 @@ public static int Main(string[] args) { if (args.Length < 3) { - args = new string[] { "public static int TestMember = 2;", "", "TestMember" }; + // args = new string[] { "public static int TestMember = 2;", "", "TestMember" }; + args = new string[] { @"public static void TestMethod() {Console.WriteLine(""from test method"");}", "", "TestMethod()" }; // return 123; } Console.WriteLine($"Testing with arguments: {string.Join(", ", args)}"); @@ -118,9 +125,9 @@ public static int Main(string[] args) File.Copy(PythonDllLocation, tempFolderPython); - CreatePythonModule(string.Format(PythonCodeStep1, args[2])); - var runnerAssembly = CreateCaseRunnerAssembly(); + CreatePythonModule(string.Format(PythonCodeStep, args[2])); { + var runnerAssembly = CreateCaseRunnerAssembly(verb:"do"); CreateTestClassAssembly(m1: args[0]); var runnerDomain = CreateDomain("case runner"); @@ -128,9 +135,10 @@ public static int Main(string[] args) } // Re-create the python module to checkup on the members - CreatePythonModule(PythonCodeStep2); + // CreatePythonModule(PythonCodeStep2); { + var runnerAssembly = CreateCaseRunnerAssembly(verb:"test"); // remove the method CreateTestClassAssembly(m1: args[1]); @@ -166,16 +174,16 @@ static string CreateTestClassAssembly(string m1 = "", string m2 = "") return CreateAssembly(name, string.Format(ChangingClassTemplate, m1, m2), exe: false); } - static string CreateCaseRunnerAssembly(string shutdownMode = "ShutdownMode.Reload") + static string CreateCaseRunnerAssembly(string shutdownMode = "ShutdownMode.Reload", string verb = "do") { - var code = string.Format(CaseRunnerTemplate, shutdownMode); + 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); + // 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 From 4edc049df160e0e3d6e6c3d8b8734f854b30b2a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Bourbonnais?= Date: Tue, 27 Oct 2020 16:29:21 -0400 Subject: [PATCH 7/7] Settle on changing the class' name --- src/domain_tests/TestRunner.cs | 85 +++++++++++++--------------------- 1 file changed, 32 insertions(+), 53 deletions(-) diff --git a/src/domain_tests/TestRunner.cs b/src/domain_tests/TestRunner.cs index 169cea56b..33c203ba0 100644 --- a/src/domain_tests/TestRunner.cs +++ b/src/domain_tests/TestRunner.cs @@ -24,48 +24,28 @@ class TestRunner using System; namespace TestNamespace -{{ +{ [Serializable] - public class TestClass - {{ - {0} - {1} - }} -}}"; - -// /// -// /// The Python code that accesses the test class in the first step of the run -// /// -// const string PythonCodeStep1 = @"import clr -// clr.AddReference('TestClass') -// import sys -// from TestNamespace import TestClass -// foo = None -// def do_work(): -// global foo -// obj = TestClass() -// foo = TestClass.{0} -// sys.my_obj = foo -// print(sys.my_obj) -// "; + public class {class} + { + } +}"; /// /// The Python code that accesses the test class /// - const string PythonCodeStep = @"import clr + const string PythonCode = @"import clr clr.AddReference('TestClass') import sys -from TestNamespace import TestClass +from TestNamespace import {class} import TestNamespace foo = None def do_work(): - obj = TestClass() - foo = TestClass.{0} - sys.my_obj = foo - print(sys.my_obj) + sys.my_obj = {class} def test_work(): - print(foo) + print({class}) + print(sys.my_obj) "; /// @@ -84,16 +64,15 @@ 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""); - // // Console.WriteLine(""verb: {1}""); - // // test_mod.{1}_work(); - // }} + 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) @@ -109,10 +88,9 @@ public static int Main() public static int Main(string[] args) { - if (args.Length < 3) + if (args.Length < 1) { - // args = new string[] { "public static int TestMember = 2;", "", "TestMember" }; - args = new string[] { @"public static void TestMethod() {Console.WriteLine(""from test method"");}", "", "TestMethod()" }; + args = new string[] {"TestClass", "NewTestClass"}; // return 123; } Console.WriteLine($"Testing with arguments: {string.Join(", ", args)}"); @@ -125,22 +103,19 @@ public static int Main(string[] args) File.Copy(PythonDllLocation, tempFolderPython); - CreatePythonModule(string.Format(PythonCodeStep, args[2])); + CreatePythonModule(args[0]); { var runnerAssembly = CreateCaseRunnerAssembly(verb:"do"); - CreateTestClassAssembly(m1: args[0]); + CreateTestClassAssembly(className: args[0]); var runnerDomain = CreateDomain("case runner"); RunAndUnload(runnerDomain, runnerAssembly); } - // Re-create the python module to checkup on the members - // CreatePythonModule(PythonCodeStep2); - { var runnerAssembly = CreateCaseRunnerAssembly(verb:"test"); // remove the method - CreateTestClassAssembly(m1: args[1]); + CreateTestClassAssembly(className: args[1]); // Do it twice for good measure { @@ -168,16 +143,19 @@ static void RunAndUnload(AppDomain domain, string assemblyPath) GC.Collect(); } - static string CreateTestClassAssembly(string m1 = "", string m2 = "") + static string CreateTestClassAssembly(string className) { var name = "TestClass.dll"; - return CreateAssembly(name, string.Format(ChangingClassTemplate, m1, m2), exe: false); + 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); } @@ -228,10 +206,11 @@ static AppDomain CreateDomain(string name) $"My Domain {name}", currentDomain.Evidence, domainsetup); + return domain; } - static string CreatePythonModule(string code) + static string CreatePythonModule(string className) { var modulePath = Path.Combine(Path.GetTempPath(), "domain_test_module"); if (Directory.Exists(modulePath)) @@ -243,7 +222,7 @@ static string CreatePythonModule(string code) 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(code); + writer.Write(PythonCode.Replace("{class}", className)); } return null;