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; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy