// 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;
using System.Linq;
namespace Python.DomainReloadTests
{
///
/// This class provides an executable that can run domain reload tests.
/// The setup is a bit complicated:
/// 1. pytest runs test_*.py in this directory.
/// 2. test_classname runs Python.DomainReloadTests.exe (this class) with an argument
/// 3. This class at runtime creates a directory that has both C# and
/// python code, and compiles the C#.
/// 4. This class then runs the C# code.
///
/// But there's a bit more indirection. This class compiles a DLL that
/// contains code that will change.
/// Then, the test case:
/// * Compiles some code, loads it into a domain, runs python that refers to it.
/// * Unload the domain, re-runs the domain to make sure domain reload happens correctly.
/// * Compile a new piece of code, load it into a new domain, run a new piece of
/// Python code to test the objects after they've been deleted or modified in C#.
/// * Unload the domain. Reload the domain, run the same python again.
///
/// This class gets built into an executable which takes one argument:
/// which test case to run. That's because pytest assumes we'll run
/// everything in one process, but we really want a clean process on each
/// test case to test the init/reload/teardown parts of the domain reload.
///
/// ### Debugging tips: ###
/// * Running pytest with the `-s` argument prevents stdout capture by pytest
/// * Add a sleep into the python test case before the crash/failure, then while
/// sleeping, attach the debugger to the Python.TestDomainReload.exe process.
///
///
class TestRunner
{
const string TestAssemblyName = "DomainTests";
class TestCase
{
///
/// The key to pass as an argument to choose this test.
///
public string Name;
public override string ToString() => Name;
///
/// The C# code to run in the first domain.
///
public string DotNetBefore;
///
/// The C# code to run in the second domain.
///
public string DotNetAfter;
///
/// The Python code to run as a module that imports the C#.
/// It should have two functions: before_reload() and after_reload().
/// Before will be called twice when DotNetBefore is loaded;
/// after will also be called twice when DotNetAfter is loaded.
/// To make the test fail, have those functions raise exceptions.
///
/// Make sure there's no leading spaces since Python cares.
///
public string PythonCode;
}
static TestCase[] Cases = new TestCase[]
{
new TestCase
{
Name = "class_rename",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Before { }
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class After { }
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
import TestNamespace
def before_reload():
sys.my_cls = TestNamespace.Before
def after_reload():
assert sys.my_cls is not None
try:
foo = TestNamespace.Before
except AttributeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
",
},
new TestCase
{
Name = "static_member_rename",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls { public static int Before() { return 5; } }
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls { public static int After() { return 10; } }
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
import TestNamespace
def before_reload():
if not hasattr(sys, 'my_cls'):
sys.my_cls = TestNamespace.Cls
sys.my_fn = TestNamespace.Cls.Before
assert 5 == sys.my_fn()
assert 5 == TestNamespace.Cls.Before()
def after_reload():
# We should have reloaded the class so we can access the new function.
assert 10 == sys.my_cls.After()
assert True is True
try:
# We should have reloaded the class. The old function still exists, but is now invalid.
sys.my_cls.Before()
except AttributeError:
print('Caught expected TypeError')
else:
raise AssertionError('Failed to throw exception: expected TypeError calling class member that no longer exists')
assert sys.my_fn is not None
try:
# Unbound functions still exist. They will error out when called though.
sys.my_fn()
except TypeError:
print('Caught expected TypeError')
else:
raise AssertionError('Failed to throw exception: expected TypeError calling unbound .NET function that no longer exists')
",
},
new TestCase
{
Name = "member_rename",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls { public int Before() { return 5; } }
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls { public int After() { return 10; } }
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
import TestNamespace
def before_reload():
sys.my_cls = TestNamespace.Cls()
sys.my_fn = TestNamespace.Cls().Before
sys.my_fn()
TestNamespace.Cls().Before()
def after_reload():
# We should have reloaded the class so we can access the new function.
assert 10 == sys.my_cls.After()
assert True is True
try:
# We should have reloaded the class. The old function still exists, but is now invalid.
sys.my_cls.Before()
except AttributeError:
print('Caught expected TypeError')
else:
raise AssertionError('Failed to throw exception: expected TypeError calling class member that no longer exists')
assert sys.my_fn is not None
try:
# Unbound functions still exist. They will error out when called though.
sys.my_fn()
except TypeError:
print('Caught expected TypeError')
else:
raise AssertionError('Failed to throw exception: expected TypeError calling unbound .NET function that no longer exists')
",
},
new TestCase
{
Name = "field_rename",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
static public int Before = 2;
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
static public int After = 4;
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
from TestNamespace import Cls
def before_reload():
sys.my_int = Cls.Before
def after_reload():
print(sys.my_int)
try:
assert 2 == Cls.Before
except AttributeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
",
},
new TestCase
{
Name = "property_rename",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
static public int Before { get { return 2; } }
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
static public int After { get { return 4; } }
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
from TestNamespace import Cls
def before_reload():
sys.my_int = Cls.Before
def after_reload():
print(sys.my_int)
try:
assert 2 == Cls.Before
except AttributeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
",
},
new TestCase
{
Name = "event_rename",
DotNetBefore = @"
using System;
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static event Action Before;
public static void Call()
{
if (Before != null) Before();
}
}
}",
DotNetAfter = @"
using System;
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static event Action After;
public static void Call()
{
if (After != null) After();
}
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
from TestNamespace import Cls
called = False
before_reload_called = False
after_reload_called = False
def callback_function():
global called
called = True
def before_reload():
global called, before_reload_called
called = False
Cls.Before += callback_function
Cls.Call()
assert called is True
before_reload_called = True
def after_reload():
global called, after_reload_called, before_reload_called
assert before_reload_called is True
if not after_reload_called:
assert called is True
after_reload_called = True
called = False
Cls.Call()
assert called is False
",
},
new TestCase
{
Name = "namespace_rename",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public int Foo;
public Cls(int i)
{
Foo = i;
}
}
}",
DotNetAfter = @"
namespace NewTestNamespace
{
[System.Serializable]
public class Cls
{
public int Foo;
public Cls(int i)
{
Foo = i;
}
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
import TestNamespace
def before_reload():
sys.my_cls = TestNamespace.Cls
sys.my_inst = TestNamespace.Cls(1)
def after_reload():
try:
TestNamespace.Cls(2)
except AttributeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
",
},
new TestCase
{
Name = "field_visibility_change",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static int Foo = 1;
public static int Field = 2;
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static int Foo = 1;
private static int Field = 2;
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
from TestNamespace import Cls
def before_reload():
assert 2 == Cls.Field
assert 1 == Cls.Foo
def after_reload():
assert 1 == Cls.Foo
try:
assert 1 == Cls.Field
except AttributeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
",
},
new TestCase
{
Name = "method_visibility_change",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static int Foo() { return 1; }
public static int Function() { return 2; }
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static int Foo() { return 1; }
private static int Function() { return 2; }
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
from TestNamespace import Cls
def before_reload():
sys.my_func = Cls.Function
assert 1 == Cls.Foo()
assert 2 == Cls.Function()
def after_reload():
assert 1 == Cls.Foo()
try:
assert 2 == Cls.Function()
except AttributeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
try:
assert 2 == sys.my_func()
except TypeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
",
},
new TestCase
{
Name = "property_visibility_change",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static int Foo { get { return 1; } }
public static int Property { get { return 2; } }
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static int Foo { get { return 1; } }
private static int Property { get { return 2; } }
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
from TestNamespace import Cls
def before_reload():
assert 1 == Cls.Foo
assert 2 == Cls.Property
def after_reload():
assert 1 == Cls.Foo
try:
assert 2 == Cls.Property
except AttributeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
",
},
new TestCase
{
Name = "class_visibility_change",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class PublicClass { }
[System.Serializable]
public class Cls { }
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
internal class Cls { }
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
import TestNamespace
def before_reload():
sys.my_cls = TestNamespace.Cls
def after_reload():
sys.my_cls()
try:
TestNamespace.Cls()
except AttributeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
",
},
new TestCase
{
Name = "method_parameters_change",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static void MyFunction(int a)
{
System.Console.WriteLine(string.Format(""MyFunction says: {0}"", a));
}
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static void MyFunction(string a)
{
System.Console.WriteLine(string.Format(""MyFunction says: {0}"", a));
}
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
from TestNamespace import Cls
def before_reload():
sys.my_cls = Cls
sys.my_func = Cls.MyFunction
sys.my_cls.MyFunction(1)
sys.my_func(2)
def after_reload():
try:
sys.my_cls.MyFunction(1)
except TypeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
try:
sys.my_func(2)
except TypeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
# Calling the function from the class passes
sys.my_cls.MyFunction('test')
try:
# calling the callable directly fails
sys.my_func('test')
except TypeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
Cls.MyFunction('another test')
",
},
new TestCase
{
Name = "method_return_type_change",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static int MyFunction()
{
return 2;
}
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
public static string MyFunction()
{
return ""22"";
}
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
from TestNamespace import Cls
def before_reload():
sys.my_cls = Cls
sys.my_func = Cls.MyFunction
assert 2 == sys.my_cls.MyFunction()
assert 2 == sys.my_func()
def after_reload():
assert '22' == sys.my_cls.MyFunction()
assert '22' == sys.my_func()
assert '22' == Cls.MyFunction()
",
},
new TestCase
{
Name = "field_type_change",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
static public int Field = 2;
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Cls
{
static public string Field = ""22"";
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
from TestNamespace import Cls
def before_reload():
sys.my_cls = Cls
assert 2 == sys.my_cls.Field
def after_reload():
assert '22' == Cls.Field
assert '22' == sys.my_cls.Field
",
},
new TestCase
{
Name = "construct_removed_class",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Before { }
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class After { }
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
import TestNamespace
def before_reload():
sys.my_cls = TestNamespace.Before
def after_reload():
try:
bar = sys.my_cls()
except TypeError:
print('Caught expected exception')
else:
raise AssertionError('Failed to throw exception')
",
},
new TestCase
{
Name = "out_to_ref_param",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Data
{
public int num = -1;
}
[System.Serializable]
public class Cls
{
public static void MyFn (out Data a)
{
a = new Data();
a.num = 9001;
}
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Data
{
public int num = -1;
}
[System.Serializable]
public class Cls
{
public static void MyFn (ref Data a)
{
a.num = 7;
}
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
import TestNamespace
import System
def before_reload():
foo = TestNamespace.Data()
bar = TestNamespace.Cls.MyFn(foo)
assert bar.num == 9001
# foo shouldn't have changed.
assert foo.num == -1
def after_reload():
try:
# Now that the function takes a ref type, we must pass a valid object.
bar = TestNamespace.Cls.MyFn(None)
except System.NullReferenceException as e:
print('caught expected exception')
else:
raise AssertionError('failed to raise')
foo = TestNamespace.Data()
bar = TestNamespace.Cls.MyFn(foo)
# foo should have changed
assert foo.num == 7
assert bar.num == 7
# Pythonnet also returns a new object with `ref`-qualified parameters
assert foo is not bar
",
},
new TestCase
{
Name = "ref_to_out_param",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Data
{
public int num = -1;
}
[System.Serializable]
public class Cls
{
public static void MyFn (ref Data a)
{
a.num = 7;
}
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Data
{
public int num = -1;
}
[System.Serializable]
public class Cls
{
public static void MyFn (out Data a)
{
a = new Data();
a.num = 9001;
}
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
import TestNamespace
import System
def before_reload():
foo = TestNamespace.Data()
bar = TestNamespace.Cls.MyFn(foo)
# foo should have changed
assert foo.num == 7
assert bar.num == 7
def after_reload():
foo = TestNamespace.Data()
bar = TestNamespace.Cls.MyFn(foo)
assert bar.num == 9001
# foo shouldn't have changed.
assert foo.num == -1
# this should work too
baz = TestNamespace.Cls.MyFn(None)
assert baz.num == 9001
",
},
new TestCase
{
Name = "ref_to_in_param",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Data
{
public int num = -1;
}
[System.Serializable]
public class Cls
{
public static void MyFn (ref Data a)
{
a.num = 7;
System.Console.Write(""Method with ref parameter: "");
System.Console.WriteLine(a.num);
}
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Data
{
public int num = -1;
}
[System.Serializable]
public class Cls
{
public static void MyFn (Data a)
{
System.Console.Write(""Method with in parameter: "");
System.Console.WriteLine(a.num);
}
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
import TestNamespace
import System
def before_reload():
foo = TestNamespace.Data()
bar = TestNamespace.Cls.MyFn(foo)
# foo should have changed
assert foo.num == 7
assert bar.num == 7
def after_reload():
foo = TestNamespace.Data()
TestNamespace.Cls.MyFn(foo)
# foo should not have changed
assert foo.num == TestNamespace.Data().num
",
},
new TestCase
{
Name = "in_to_ref_param",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class Data
{
public int num = -1;
}
[System.Serializable]
public class Cls
{
public static void MyFn (Data a)
{
System.Console.Write(""Method with in parameter: "");
System.Console.WriteLine(a.num);
}
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class Data
{
public int num = -1;
}
[System.Serializable]
public class Cls
{
public static void MyFn (ref Data a)
{
a.num = 7;
System.Console.Write(""Method with ref parameter: "");
System.Console.WriteLine(a.num);
}
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
import TestNamespace
import System
def before_reload():
foo = TestNamespace.Data()
TestNamespace.Cls.MyFn(foo)
# foo should not have changed
assert foo.num == TestNamespace.Data().num
def after_reload():
foo = TestNamespace.Data()
bar = TestNamespace.Cls.MyFn(foo)
# foo should have changed
assert foo.num == 7
assert bar.num == 7
",
},
new TestCase
{
Name = "nested_type",
DotNetBefore = @"
namespace TestNamespace
{
[System.Serializable]
public class WithNestedType
{
[System.Serializable]
public class Inner
{
public static int Value = -1;
}
}
}",
DotNetAfter = @"
namespace TestNamespace
{
[System.Serializable]
public class WithNestedType
{
[System.Serializable]
public class Inner
{
public static int Value = -1;
}
}
}",
PythonCode = @"
import clr
import sys
clr.AddReference('DomainTests')
import TestNamespace
def before_reload():
sys.my_obj = TestNamespace.WithNestedType
def after_reload():
assert sys.my_obj is not None
foo = sys.my_obj.Inner()
print(foo)
",
},
new TestCase
{
// The C# code for this test doesn't matter; we're testing
// that the import hook behaves properly after a domain reload
Name = "import_after_reload",
DotNetBefore = "",
DotNetAfter = "",
PythonCode = @"
import sys
def before_reload():
import clr
import System
def after_reload():
assert 'System' in sys.modules
assert 'clr' in sys.modules
import clr
import System
",
},
new TestCase
{
Name = "test_serialize_unserializable_object",
DotNetBefore = @"
namespace TestNamespace
{
public class NotSerializableTextWriter : System.IO.TextWriter
{
override public System.Text.Encoding Encoding { get { return System.Text.Encoding.ASCII;} }
}
[System.Serializable]
public static class SerializableWriter
{
private static System.IO.TextWriter _writer = null;
public static System.IO.TextWriter Writer {get { return _writer; }}
public static void CreateInternalWriter()
{
_writer = System.IO.TextWriter.Synchronized(new NotSerializableTextWriter());
}
}
}
",
DotNetAfter = @"
namespace TestNamespace
{
public class NotSerializableTextWriter : System.IO.TextWriter
{
override public System.Text.Encoding Encoding { get { return System.Text.Encoding.ASCII;} }
}
[System.Serializable]
public static class SerializableWriter
{
private static System.IO.TextWriter _writer = null;
public static System.IO.TextWriter Writer {get { return _writer; }}
public static void CreateInternalWriter()
{
_writer = System.IO.TextWriter.Synchronized(new NotSerializableTextWriter());
}
}
}
",
PythonCode = @"
import sys
def before_reload():
import clr
import System
clr.AddReference('DomainTests')
import TestNamespace
TestNamespace.SerializableWriter.CreateInternalWriter();
sys.__obj = TestNamespace.SerializableWriter.Writer
sys.__obj.WriteLine('test')
def after_reload():
import clr
import System
sys.__obj.WriteLine('test')
",
}
};
///
/// The runner's code. Runs the python code
/// This is a template for string.Format
/// Arg 0 is the no-arg python function to run, before or after.
///
const string CaseRunnerTemplate = @"
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using Python.Runtime;
namespace Serialization
{{
// Classes in this namespace is mostly useful for test_serialize_unserializable_object
class NotSerializableSerializer : ISerializationSurrogate
{{
public NotSerializableSerializer()
{{
}}
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{{
info.AddValue(""notSerialized_tp"", obj.GetType());
}}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{{
if (info == null)
{{
return null;
}}
Type typeObj = info.GetValue(""notSerialized_tp"", typeof(Type)) as Type;
if (typeObj == null)
{{
return null;
}}
obj = Activator.CreateInstance(typeObj);
return obj;
}}
}}
class NonSerializableSelector : SurrogateSelector
{{
public override ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector)
{{
if (type == null)
{{
throw new ArgumentNullException();
}}
selector = (ISurrogateSelector)this;
if (type.IsSerializable)
{{
return null; // use whichever default
}}
else
{{
return (ISerializationSurrogate)(new NotSerializableSerializer());
}}
}}
}}
}}
namespace CaseRunner
{{
class CaseRunner
{{
public static int Main()
{{
try
{{
RuntimeData.FormatterFactory = () =>
{{
return new BinaryFormatter(){{SurrogateSelector = new Serialization.NonSerializableSelector()}};
}};
PythonEngine.Initialize();
using (Py.GIL())
{{
var temp = AppDomain.CurrentDomain.BaseDirectory;
dynamic sys = Py.Import(""sys"");
sys.path.append(new PyString(temp));
dynamic test_mod = Py.Import(""domain_test_module.mod"");
test_mod.{0}_reload();
}}
PythonEngine.Shutdown();
}}
catch (PythonException pe)
{{
throw new ArgumentException(message:pe.Message+"" ""+pe.StackTrace);
}}
catch (Exception e)
{{
Console.Error.WriteLine(e.StackTrace);
throw;
}}
return 0;
}}
}}
}}
";
readonly static string PythonDllLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Python.Runtime.dll");
static string TestPath = null;
public static int Main(string[] args)
{
if (args.Length < 1)
{
foreach (var testCase in Cases)
{
Run(testCase);
Console.WriteLine();
}
}
else
{
string testName = args[0];
Console.WriteLine($"-- Looking for domain reload test case {testName}");
var testCase = int.TryParse(testName, out var index) ? Cases[index] : Cases.First(c => c.Name == testName);
Run(testCase);
}
return 0;
}
static void Run(TestCase testCase)
{
Console.WriteLine($"-- Running domain reload test case: {testCase.Name}");
SetupTestFolder(testCase.Name);
CreatePythonModule(testCase);
{
var runnerAssembly = CreateCaseRunnerAssembly(verb:"before");
CreateTestClassAssembly(testCase.DotNetBefore);
{
var runnerDomain = CreateDomain("case runner before");
RunAndUnload(runnerDomain, runnerAssembly);
}
{
var runnerDomain = CreateDomain("case runner before (again)");
RunAndUnload(runnerDomain, runnerAssembly);
}
}
{
var runnerAssembly = CreateCaseRunnerAssembly(verb:"after");
CreateTestClassAssembly(testCase.DotNetAfter);
// Do it twice for good measure
{
var runnerDomain = CreateDomain("case runner after");
RunAndUnload(runnerDomain, runnerAssembly);
}
{
var runnerDomain = CreateDomain("case runner after (again)");
RunAndUnload(runnerDomain, runnerAssembly);
}
}
// Don't delete unconditionally. It's sometimes useful to leave the
// folder behind to debug failing tests.
TeardownTestFolder();
Console.WriteLine($"-- PASSED: {testCase.Name}");
}
static void SetupTestFolder(string testCaseName)
{
var pid = System.Diagnostics.Process.GetCurrentProcess().Id;
TestPath = Path.Combine(Path.GetTempPath(), $"Python.TestRunner.{testCaseName}-{pid}");
if (Directory.Exists(TestPath))
{
Directory.Delete(TestPath, recursive: true);
}
Directory.CreateDirectory(TestPath);
Console.WriteLine($"Using directory: {TestPath}");
File.Copy(PythonDllLocation, Path.Combine(TestPath, "Python.Runtime.dll"));
}
static void TeardownTestFolder()
{
if (Directory.Exists(TestPath))
{
Directory.Delete(TestPath, recursive: true);
}
}
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($"-- Running domain {domain.FriendlyName}");
domain.ExecuteAssembly(assemblyPath);
AppDomain.Unload(domain);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
static string CreateTestClassAssembly(string code)
{
return CreateAssembly(TestAssemblyName + ".dll", code, exe: false);
}
static string CreateCaseRunnerAssembly(string verb)
{
var code = string.Format(CaseRunnerTemplate, verb);
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(TestPath, assemblyName);
parameters.OutputAssembly = assemblyFullPath;
parameters.ReferencedAssemblies.Add("System.dll");
parameters.ReferencedAssemblies.Add("System.Core.dll");
parameters.ReferencedAssemblies.Add("Microsoft.CSharp.dll");
var netstandard = "netstandard.dll";
if (Type.GetType("Mono.Runtime") != null)
{
netstandard = "Facades/" + netstandard;
}
parameters.ReferencedAssemblies.Add(netstandard);
parameters.ReferencedAssemblies.Add(PythonDllLocation);
// Write code to file so it can debugged.
var sourcePath = Path.Combine(TestPath, name+"_source.cs");
using(var file = new StreamWriter(sourcePath))
{
file.Write(code);
}
CompilerResults results = provider.CompileAssemblyFromFile(parameters, sourcePath);
if (results.NativeCompilerReturnValue != 0)
{
var stderr = System.Console.Error;
stderr.WriteLine($"Error in {name} compiling:\n{code}");
foreach (var error in results.Errors)
{
stderr.WriteLine(error);
}
throw new ArgumentException("Error compiling code");
}
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 = TestPath,
ConfigurationFile = currentDomain.SetupInformation.ConfigurationFile,
LoaderOptimization = LoaderOptimization.SingleDomain,
PrivateBinPath = "."
};
var domain = AppDomain.CreateDomain(
$"My Domain {name}",
currentDomain.Evidence,
domainsetup);
return domain;
}
static string CreatePythonModule(TestCase testCase)
{
var modulePath = Path.Combine(TestPath, "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(testCase.PythonCode);
}
return null;
}
}
}