Skip to content

Commit 98b60ec

Browse files
benoithudsonBadSingleton
benoithudson
authored andcommitted
Generalize the test runner
Move the logic of what each test does into TestRunner.cs so we can control exactly what code gets compiled. test_domain_reload now just says to run each test one by one.
1 parent d830a45 commit 98b60ec

File tree

2 files changed

+120
-63
lines changed

2 files changed

+120
-63
lines changed

src/domain_tests/TestRunner.cs

Lines changed: 112 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,105 @@
44
using Microsoft.CSharp;
55
using System.CodeDom.Compiler;
66
using System.IO;
7+
using System.Linq;
78

89
namespace Python.DomainReloadTests
910
{
1011
/// <summary>
12+
/// This class provides an executable that can run domain reload tests.
13+
/// The setup is a bit complicated:
14+
/// 1. pytest runs test_*.py in this directory.
15+
/// 2. test_classname runs Python.DomainReloadTests.exe (this class) with an argument
16+
/// 3. This class at runtime creates a directory that has both C# and
17+
/// python code, and compiles the C#.
18+
/// 4. This class then runs the C# code.
19+
///
20+
/// But wait there's more indirection. The C# code that's run -- known as
21+
/// the test runner --
1122
/// This class compiles a DLL that contains the class which code will change
1223
/// and a runner executable that will run Python code referencing the class.
13-
/// It's Main() will:
14-
/// * Run the runner and unlod it's domain
15-
/// * Modify and re-compile the test class
16-
/// * Re-run the runner and unload it twice
24+
/// Each test case:
25+
/// * Compiles some code, loads it into a domain, runs python that refers to it.
26+
/// * Unload the domain.
27+
/// * Compile a new piece of code, load it into a domain, run a new piece of python that accesses the code.
28+
/// * Unload the domain. Reload the domain, run the same python again.
29+
/// This class gets built into an executable which takes one argument:
30+
/// which test case to run. That's because pytest assumes we'll run
31+
/// everything in one process, but we really want a clean process on each
32+
/// test case to test the init/reload/teardown parts of the domain reload
33+
/// code.
1734
/// </summary>
1835
class TestRunner
1936
{
20-
/// <summary>
21-
/// The code of the test class that changes
22-
/// </summary>
23-
const string ChangingClassTemplate = @"
24-
using System;
37+
const string TestAssemblyName = "DomainTests";
2538

26-
namespace TestNamespace
27-
{
28-
[Serializable]
29-
public class {class}
30-
{
31-
}
32-
}";
39+
class TestCase
40+
{
41+
/// <summary>
42+
/// The key to pass as an argument to choose this test.
43+
/// </summary>
44+
public string Name;
3345

34-
/// <summary>
35-
/// The Python code that accesses the test class
36-
/// </summary>
37-
const string PythonCode = @"import clr
38-
clr.AddReference('TestClass')
39-
import sys
40-
from TestNamespace import {class}
41-
import TestNamespace
42-
foo = None
43-
def do_work():
44-
sys.my_obj = {class}
45-
46-
def test_work():
47-
print({class})
48-
print(sys.my_obj)
49-
";
46+
/// <summary>
47+
/// The C# code to run in the first domain.
48+
/// </summary>
49+
public string DotNetBefore;
50+
51+
/// <summary>
52+
/// The C# code to run in the second domain.
53+
/// </summary>
54+
public string DotNetAfter;
55+
56+
/// <summary>
57+
/// The Python code to run as a module that imports the C#.
58+
/// It should have two functions: before() and after(). Before
59+
/// will be called when DotNetBefore is loaded; after will be
60+
/// called (twice) when DotNetAfter is loaded.
61+
/// To make the test fail, have those functions raise exceptions.
62+
///
63+
/// Make sure there's no leading spaces since Python cares.
64+
/// </summary>
65+
public string PythonCode;
66+
}
67+
68+
static TestCase[] Cases = new TestCase[]
69+
{
70+
new TestCase {
71+
Name = "class_rename",
72+
DotNetBefore = @"
73+
namespace TestNamespace {
74+
[System.Serializable]
75+
public class Before { }
76+
}",
77+
DotNetAfter = @"
78+
namespace TestNamespace {
79+
[System.Serializable]
80+
public class After { }
81+
}",
82+
PythonCode = @"
83+
import clr
84+
clr.AddReference('DomainTests')
85+
from TestNamespace import Before
86+
def before():
87+
import sys
88+
sys.my_cls = Before
89+
def after():
90+
import sys
91+
try:
92+
sys.my_cls.Member()
93+
except TypeError:
94+
print('Caught expected exception')
95+
else:
96+
raise AssertionError('Failed to throw exception')
97+
",
98+
}
99+
};
50100

51101
/// <summary>
52102
/// The runner's code. Runs the python code
103+
/// This is a template for string.Format
104+
/// Arg 0 is the reload mode: ShutdownMode.Reload or other.
105+
/// Arg 1 is the no-arg python function to run, before or after.
53106
/// </summary>
54107
const string CaseRunnerTemplate = @"
55108
using System;
@@ -71,7 +124,7 @@ public static int Main()
71124
dynamic sys = Py.Import(""sys"");
72125
sys.path.append(new PyString(temp));
73126
dynamic test_mod = Py.Import(""domain_test_module.mod"");
74-
test_mod.{1}_work();
127+
test_mod.{1}();
75128
}}
76129
PythonEngine.Shutdown();
77130
}}
@@ -93,12 +146,18 @@ public static int Main()
93146

94147
public static int Main(string[] args)
95148
{
149+
TestCase testCase;
96150
if (args.Length < 1)
97151
{
98-
args = new string[] {"TestClass", "NewTestClass"};
99-
// return 123;
152+
testCase = Cases[0];
153+
}
154+
else
155+
{
156+
string testName = args[0];
157+
Console.WriteLine($"Looking for domain reload test case {testName}");
158+
testCase = Cases.First(c => c.Name == testName);
100159
}
101-
Console.WriteLine($"Testing with arguments: {string.Join(", ", args)}");
160+
Console.WriteLine($"Running domain reload test case: {testCase.Name}");
102161

103162
var tempFolderPython = Path.Combine(Path.GetTempPath(), "Python.Runtime.dll");
104163
if (File.Exists(tempFolderPython))
@@ -108,27 +167,26 @@ public static int Main(string[] args)
108167

109168
File.Copy(PythonDllLocation, tempFolderPython);
110169

111-
CreatePythonModule(args[0]);
170+
CreatePythonModule(testCase);
112171
{
113-
var runnerAssembly = CreateCaseRunnerAssembly(verb:"do");
114-
CreateTestClassAssembly(className: args[0]);
172+
var runnerAssembly = CreateCaseRunnerAssembly(verb:"before");
173+
CreateTestClassAssembly(testCase.DotNetBefore);
115174

116-
var runnerDomain = CreateDomain("case runner");
175+
var runnerDomain = CreateDomain("case runner before");
117176
RunAndUnload(runnerDomain, runnerAssembly);
118177
}
119178

120179
{
121-
var runnerAssembly = CreateCaseRunnerAssembly(verb:"test");
122-
// remove the method
123-
CreateTestClassAssembly(className: args[1]);
180+
var runnerAssembly = CreateCaseRunnerAssembly(verb:"after");
181+
CreateTestClassAssembly(testCase.DotNetAfter);
124182

125183
// Do it twice for good measure
126184
{
127-
var runnerDomain = CreateDomain("case runner 2");
185+
var runnerDomain = CreateDomain("case runner after");
128186
RunAndUnload(runnerDomain, runnerAssembly);
129187
}
130188
{
131-
var runnerDomain = CreateDomain("case runner 3");
189+
var runnerDomain = CreateDomain("case runner after (again)");
132190
RunAndUnload(runnerDomain, runnerAssembly);
133191
}
134192
}
@@ -148,15 +206,12 @@ static void RunAndUnload(AppDomain domain, string assemblyPath)
148206
GC.Collect();
149207
}
150208

151-
static string CreateTestClassAssembly(string className)
209+
static string CreateTestClassAssembly(string code)
152210
{
153-
var name = "TestClass.dll";
154-
string code = ChangingClassTemplate.Replace("{class}", className);
155-
156-
return CreateAssembly(name, code, exe: false);
211+
return CreateAssembly(TestAssemblyName + ".dll", code, exe: false);
157212
}
158213

159-
static string CreateCaseRunnerAssembly(string shutdownMode = "ShutdownMode.Reload", string verb = "do")
214+
static string CreateCaseRunnerAssembly(string verb, string shutdownMode = "ShutdownMode.Reload")
160215
{
161216
var code = string.Format(CaseRunnerTemplate, shutdownMode, verb);
162217
var name = "TestCaseRunner.exe";
@@ -184,11 +239,13 @@ static string CreateAssembly(string name, string code, bool exe = false)
184239
CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
185240
if (results.NativeCompilerReturnValue != 0)
186241
{
242+
var stderr = System.Console.Error;
243+
stderr.WriteLine($"Error in {name} compiling:\n{code}");
187244
foreach (var error in results.Errors)
188245
{
189-
System.Console.WriteLine(error);
246+
stderr.WriteLine(error);
190247
}
191-
throw new ArgumentException();
248+
throw new ArgumentException("Error compiling code");
192249
}
193250

194251
return assemblyFullPath;
@@ -215,7 +272,7 @@ static AppDomain CreateDomain(string name)
215272
return domain;
216273
}
217274

218-
static string CreatePythonModule(string className)
275+
static string CreatePythonModule(TestCase testCase)
219276
{
220277
var modulePath = Path.Combine(Path.GetTempPath(), "domain_test_module");
221278
if (Directory.Exists(modulePath))
@@ -227,7 +284,7 @@ static string CreatePythonModule(string className)
227284
File.Create(Path.Combine(modulePath, "__init__.py")).Close(); //Create and don't forget to close!
228285
using (var writer = File.CreateText(Path.Combine(modulePath, "mod.py")))
229286
{
230-
writer.Write(PythonCode.Replace("{class}", className));
287+
writer.Write(testCase.PythonCode);
231288
}
232289

233290
return null;
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import subprocess
22
import os
33

4-
def runit(m1, m2, member):
5-
proc = subprocess.Popen([os.path.join(os.path.split(__file__)[0], 'bin', 'Python.DomainReloadTests.exe'), m1, m2, member])
4+
def _run_test(testname):
5+
dirname = os.path.split(__file__)[0]
6+
exename = os.path.join(dirname, 'bin', 'Python.DomainReloadTests.exe'),
7+
proc = subprocess.Popen([
8+
exename,
9+
testname,
10+
])
611
proc.wait()
712

813
assert proc.returncode == 0
914

1015
def test_rename_class():
11-
12-
m1 = 'TestClass'
13-
m2 = 'TestClass2'
14-
member = ''
15-
runit(m1, m2, member)
16-
16+
_run_test('class_rename')

0 commit comments

Comments
 (0)