4
4
using Microsoft . CSharp ;
5
5
using System . CodeDom . Compiler ;
6
6
using System . IO ;
7
+ using System . Linq ;
7
8
8
9
namespace Python . DomainReloadTests
9
10
{
10
11
/// <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 --
11
22
/// This class compiles a DLL that contains the class which code will change
12
23
/// 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.
17
34
/// </summary>
18
35
class TestRunner
19
36
{
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" ;
25
38
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 ;
33
45
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
+ } ;
50
100
51
101
/// <summary>
52
102
/// 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.
53
106
/// </summary>
54
107
const string CaseRunnerTemplate = @"
55
108
using System;
@@ -71,7 +124,7 @@ public static int Main()
71
124
dynamic sys = Py.Import(""sys"");
72
125
sys.path.append(new PyString(temp));
73
126
dynamic test_mod = Py.Import(""domain_test_module.mod"");
74
- test_mod.{1}_work ();
127
+ test_mod.{1}();
75
128
}}
76
129
PythonEngine.Shutdown();
77
130
}}
@@ -93,12 +146,18 @@ public static int Main()
93
146
94
147
public static int Main ( string [ ] args )
95
148
{
149
+ TestCase testCase ;
96
150
if ( args . Length < 1 )
97
151
{
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 ) ;
100
159
}
101
- Console . WriteLine ( $ "Testing with arguments : { string . Join ( ", " , args ) } ") ;
160
+ Console . WriteLine ( $ "Running domain reload test case : { testCase . Name } ") ;
102
161
103
162
var tempFolderPython = Path . Combine ( Path . GetTempPath ( ) , "Python.Runtime.dll" ) ;
104
163
if ( File . Exists ( tempFolderPython ) )
@@ -108,27 +167,26 @@ public static int Main(string[] args)
108
167
109
168
File . Copy ( PythonDllLocation , tempFolderPython ) ;
110
169
111
- CreatePythonModule ( args [ 0 ] ) ;
170
+ CreatePythonModule ( testCase ) ;
112
171
{
113
- var runnerAssembly = CreateCaseRunnerAssembly ( verb : "do " ) ;
114
- CreateTestClassAssembly ( className : args [ 0 ] ) ;
172
+ var runnerAssembly = CreateCaseRunnerAssembly ( verb : "before " ) ;
173
+ CreateTestClassAssembly ( testCase . DotNetBefore ) ;
115
174
116
- var runnerDomain = CreateDomain ( "case runner" ) ;
175
+ var runnerDomain = CreateDomain ( "case runner before " ) ;
117
176
RunAndUnload ( runnerDomain , runnerAssembly ) ;
118
177
}
119
178
120
179
{
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 ) ;
124
182
125
183
// Do it twice for good measure
126
184
{
127
- var runnerDomain = CreateDomain ( "case runner 2 " ) ;
185
+ var runnerDomain = CreateDomain ( "case runner after " ) ;
128
186
RunAndUnload ( runnerDomain , runnerAssembly ) ;
129
187
}
130
188
{
131
- var runnerDomain = CreateDomain ( "case runner 3 " ) ;
189
+ var runnerDomain = CreateDomain ( "case runner after (again) " ) ;
132
190
RunAndUnload ( runnerDomain , runnerAssembly ) ;
133
191
}
134
192
}
@@ -148,15 +206,12 @@ static void RunAndUnload(AppDomain domain, string assemblyPath)
148
206
GC . Collect ( ) ;
149
207
}
150
208
151
- static string CreateTestClassAssembly ( string className )
209
+ static string CreateTestClassAssembly ( string code )
152
210
{
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 ) ;
157
212
}
158
213
159
- static string CreateCaseRunnerAssembly ( string shutdownMode = "ShutdownMode.Reload" , string verb = "do " )
214
+ static string CreateCaseRunnerAssembly ( string verb , string shutdownMode = "ShutdownMode.Reload " )
160
215
{
161
216
var code = string . Format ( CaseRunnerTemplate , shutdownMode , verb ) ;
162
217
var name = "TestCaseRunner.exe" ;
@@ -184,11 +239,13 @@ static string CreateAssembly(string name, string code, bool exe = false)
184
239
CompilerResults results = provider . CompileAssemblyFromSource ( parameters , code ) ;
185
240
if ( results . NativeCompilerReturnValue != 0 )
186
241
{
242
+ var stderr = System . Console . Error ;
243
+ stderr . WriteLine ( $ "Error in { name } compiling:\n { code } ") ;
187
244
foreach ( var error in results . Errors )
188
245
{
189
- System . Console . WriteLine ( error ) ;
246
+ stderr . WriteLine ( error ) ;
190
247
}
191
- throw new ArgumentException ( ) ;
248
+ throw new ArgumentException ( "Error compiling code" ) ;
192
249
}
193
250
194
251
return assemblyFullPath ;
@@ -215,7 +272,7 @@ static AppDomain CreateDomain(string name)
215
272
return domain ;
216
273
}
217
274
218
- static string CreatePythonModule ( string className )
275
+ static string CreatePythonModule ( TestCase testCase )
219
276
{
220
277
var modulePath = Path . Combine ( Path . GetTempPath ( ) , "domain_test_module" ) ;
221
278
if ( Directory . Exists ( modulePath ) )
@@ -227,7 +284,7 @@ static string CreatePythonModule(string className)
227
284
File . Create ( Path . Combine ( modulePath , "__init__.py" ) ) . Close ( ) ; //Create and don't forget to close!
228
285
using ( var writer = File . CreateText ( Path . Combine ( modulePath , "mod.py" ) ) )
229
286
{
230
- writer . Write ( PythonCode . Replace ( "{class}" , className ) ) ;
287
+ writer . Write ( testCase . PythonCode ) ;
231
288
}
232
289
233
290
return null ;
0 commit comments