Hail...
Just hailed like hell here, thought a window was going to break ;o)
The professional blog of Alex Henderson, Software Engineer
Just hailed like hell here, thought a window was going to break ;o)
Well, I've started working with the latest drop of Base4.net (version 2.1) - in fact I've decided to take the plunge and actually start using it for the project I'm currently working on (a work project, rather then a home project) so I should be giving it a pretty good thrashing over the next 3-6 months. I already had a rough schema in place, so I generated types from it using the bundled web admin... It seems pretty cool... though I've struck a few things:
Back again!
Here is the source for the last post: ironpython-and-nunit.zip (23.01 KB) - it's pretty rough around the edges (and in the middle :P) because I was just trying to see if it was possible, rather then to produce some production-level toolkit for integrating IronPython and NUnit.
Edit 2008-03-14: An updated version of the code target
NUnit 2.4 and VS2008 can be found here.
class NUnitFixture:
def __init__(self):
pass
def setUp(self):
pass
def tearDown(self):
pass
def setUpFixture(self):
pass
def tearDownFixture(self):
pass
def failUnlessRaises(self, excClass, callableObj, *args, **kwargs):
if issubclass(excClass, System.Exception):
try:
callableObj(*args, **kwargs)except Exception, e:
if hasattr(e, "clsException") and
(type(e.clsException) == excClass):
return
else:
try:
callableObj(*args, **kwargs)
except excClass:
return
if hasattr(excClass,'__name__'):
excName = excClass.__name__
else:excName = str(excClass)
raise AssertionException("%s not raised" % excName)
def getTestCaseNames(testCaseClass, prefix):
def isTestMethod(attrname, testCaseClass=testCaseClass, prefix=prefix):
return attrname.startswith(prefix) and callable(getattr(testCaseClass, attrname))
testFnNames = filter(isTestMethod, dir(testCaseClass))
for baseclass in testCaseClass.__bases__:
for testFnName in getTestCaseNames(baseclass, prefix):
if testFnName not in testFnNames:
testFnNames.append(testFnName)
testFnNames.sort(cmp)
return testFnNames
public class MyPythonSuite : AbstractPythonSuite
{
public MyPythonSuite()
: base("MyPythonFixture.py")
{
}
}
public class DynamicPythonSuite : AbstractPythonSuite
{
public DynamicPythonSuite()
: base(FindSuitablePythonScripts())
{
}private static string[] FindSuitablePythonScripts()
{
Listscripts = new List ();
Assembly assembly = typeof (DynamicPythonSuite).Assembly;
string indication = "#test";
foreach (string potentialScript in assembly.GetManifestResourceNames())
{
using (StreamReader reader =
new StreamReader(assembly.GetManifestResourceStream(potentialScript)))
{
if (reader.ReadLine().Trim().StartsWith(indication,
true, CultureInfo.InvariantCulture))
{
scripts.Add(potentialScript);
}
}
}
return scripts.ToArray();
}
}
I've been thinking about the issues involved with using
IronPython in a product as a scripting language, internally we've
already struggled at Syzmk with my love of refactoring breaking
scripts in our deployed products left right and centre.
I think the problems I need to solve first are:
Regression tests for expected script usage is what it comes
down... but if you sit down and start trying to write these tests
in NUnit it all gets a bit unwieldy - there's a lot of time
wasted making sure your tabs are right, escaping characters
correctly in strings, and running scripts, evaluating the
results, and then asserting against them - it's frustrating, and
fails to emulate the experience of your users who are writing
scripts purely in Python.
So my next port of call was to try the unit testing support that
comes with python (PyUnit) - it would definitely do the job - but
then I need to do a couple of things... first I need to start
bundling quite a few standard libraries with the scripting engine
in our product, and then I have to write additional functionality
to support capturing the results of running the PyUnit tests and
presenting them to the automated build process... It's all
sounding like a pain in the ass, and the automation build
works nicely as it is, I'd rather not have to mess with it...
So I decided to do a little integration work, to see if I could
run a test suite written in python as part of a C# assembly.
First off, my solutions user experience is ok - not perfect - but
then I just wanted to make sure I could get it working, before I
put any effort into cleaning it up, and not being an expert in
extending NUnit, or coding python, it was probably doomed from
the start to be a mediocre solution ;o)
The steps to get it working in a test assembly are:
First off, we write the fixture, the only requirements are:
Here's an example fixture:
import System
from System import Consoleclass MyPythonFixture(NUnitFixture):
def testPass(self):
Console.WriteLine("--testPass--")
Assert.IsTrue(True)def testFail(self):
Console.WriteLine("--testFail--")
Assert.IsTrue(False, "this will fail")
As you can see we are just using the existing NUnit Assert
methods... There is one caveat to this that I will cover in
another post (that of testing for expected exceptions).
Next we create a suite - which can wrap one or more python script
files (included as assembly resources) - this is all we need to
bridge the gap.
public class MyPythonSuite : AbstractPythonSuite
{
public MyPythonSuite()
: base("MyPythonFixture.py")
{
}
}
And last of all, we need to drop in a suite builder - in theory I
should be able to include this in a support assembly - but it
doesn't seem to work, here's the code:
[SuiteBuilder]
public class PythonSuiteExtensionBuilder : ISuiteBuilder
{
public TestSuite BuildFrom(Type type, int assemblyKey)
{
if (CanBuildFrom(type)) return new PythonSuiteExtension(type, assemblyKey);
return null;
}public bool CanBuildFrom(Type type)
{
return (typeof(AbstractPythonSuite).IsAssignableFrom(type) && (type != typeof(AbstractPythonSuite)));
}
}
At this point when can test the assembly with NUnitGui,
and see our python fixtures appear under the "MyPythonSuite"...
pretty slick huh?
border="0" />
The big downsides of this approach are that you can't run the
tests individually using something like TestDriven.Net,
and Resharper's unit test window doesn't pick them up...
However NUnitGui and the corresponding NUnitConsole
have no problems with them, and running all the tests in your
assembly with TestDriven.Net does pick them up thankfully
- so as long you don't have many slow tests in your assembly it
should still be tolerable (you can run them while still
coding...).
And of course, they will be picked up in your automated builds -
which is great news for me... I hate messing around with
CruiseControl.Net's configuration.
I'll tidy up the implementation a little and post the code
soon.
Well, Seo Sanghyeon asked me a pretty reasonable question of the last entry
"Why don't you use help(), instead of printing Overloads?"
Good question, I didn't realise that actually worked in IronPython for .Net types, I was very pleasantly surprised, I remember in the early betas i tried using help() on various managed types and it would just raise a NotImplementedException exception (or something like that, I forget now) - so what does that mean for my examples... well... let's take a system type like Guid and use help() on it.
IronPython 1.0.60816 on .NET 2.0.50727.42
Copyright (c) Microsoft Corporation. All rights reserved.
>>> import System
>>> from System import *
>>> help(Guid)
Help on Guid in module System in mscorlib, Version=2.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089
| Guid(Array[Byte] b)
| Guid(UInt32 a, UInt16 b,
UInt16 c, Byte d, Byte e, Byte f, Byte g, Byteh, Byte i, Byte j,
Byte k)
| Guid(str g)
| Guid(int a, Int16 b, Int16
c, Array[Byte] d)
| Guid(int a, Int16 b, Int16
c, Byte d, Byte e, Byte f, Byte g, Byte h, Byte i, Byte j, Byte
k)
| Guid
CreateInstance[Guid]()
|
| Data and other attributes defined here:
|
| CompareTo(...)
|
int CompareTo(self, object value)
|
int CompareTo(self, Guid value)
| Equals(...)
|
bool Equals(self, object o)
|
bool Equals(self, Guid g)
| Finalize(...)
|
Finalize(self)
| GetHashCode(...)
|
int GetHashCode(self)
| GetType(...)
|
Type GetType(self)
| MemberwiseClone(...)
|
object MemberwiseClone(self)
| NewGuid(...)
|
Guid NewGuid()
| ToByteArray(...)
|
Array[Byte] ToByteArray(self)
| ToString(...)
|
str ToString(self)
|
str ToString(self, str format)
|
str ToString(self, str format, IFormatProvider provider)
| __eq__(...)
|
bool op_Equality(Guid a, Guid b)
| __init__(...)
|
x.__init__(...) initializes x; see x.__class__.__doc__ for
signature
|
x.__init__(...) initializes x; see x.__class__.__doc__ for
signature
| __ne__(...)
|
bool op_Inequality(Guid a, Guid b)
| __new__(...)
|
__new__(cls, Array[Byte] b)
|
__new__(cls, UInt32 a, UInt16 b, UInt16 c, Byte d, Byte e, Bytef,
Byte g, Byte h, Byte i, Byte j, Byte k)
|
__new__(cls, str g)
|
__new__(cls, int a, Int16 b, Int16 c, Array[Byte] d)
|
__new__(cls, int a, Int16 b, Int16 c, Byte d, Byte e, Byte f,
Byte g, Byte h, Byte i, Byte j, Byte k)
|
Guid CreateInstance[Guid]()
|
>>>
Sweet, it works a treat - and will save a lot of digging time - though you might want to capture the results for big classes ;o)
Thanks Seo!
(btw, Seo is the man behind the Iron Python Community Edition & FePy projects - which can be found on sourceforge) and I would have to agree with this article that he does seem almost OmniPresent in the IronPython community...