This is a highly specialized short note about how I wrote write once, run everywhere tests for a library ported from C # to Java using Python.
The point is this: there is a large, thick and beautiful library that has been commercially ported from C # to Java. API remained almost the same, naming conventions naturally changed when switching to another language. We needed to write a thick stack of tests verifying that the library's clone works identically to the original (regression tests, in other words). For this, the results of the work of the library code were compared (certain binaries and xml-metadata). The tests were non-trivial, there were a lot of them, and the most unpleasant was that they were constantly written from one end by a team of four people. For some time I diligently ported them to Java, then spat and suggested that the team write tests in a language that could be immediately run on the CLR (with the old library) and the JVM (with the clone). It turned out that they themselves had been thinking about Python for some time,
and that's how it turned out.
1. Basics
Well, about the primitive. On the CLR, I run the code IronPython, on the JVM - Jython, respectively. From pleasant - Jython itself provides loadable Java-classes instead of getters-setters mechanism perpertey. Why is it nice? Because when porting to Java, Sharpee Properties were naturally replaced with getters-setters.
')
2. Where am I?
It is useful to know, on top of what runtime we are currently running. It's simple.
isDotNet = sys.version.find('IronPython') > -1
3. Connection of the necessary libraries
In IronPython and Jython, this is done differently (using the clr library in IronPython, and simply adding it to the path in Jython), and the convention for naming modules is different, so we unify:
def cpUseLibrary(lib_path, library): if isDotNet: sys.path.append(lib_path) import clr clr.AddReference('OurProduct20.' + library) else: sys.path.append(lib_path + '\\java_libs\\ourproduct20.' + library.lower() + '.jar')
This is used in the working code in the following stupid way:
cpUseLibrary(path_to_bins, 'Core') cpUseLibrary(path_to_bins, 'Formats.Common')
4. Download the required classes.
Here is a small problem. In general, both interpreters allow stupidly importing from the necessary namespaces / packages, but we need a dynamic construction, taking into account, again, different conventions for naming packages.
def cpImport(module, clazz, globs = globals()):
The point is to use __import __ () as the Python internal mechanism for importing from modules. Actually, the entry “import from somewhere from something” in __import __ () eventually unfolds. The current problem that the hands did not reach is to solve - it is the need to transfer globals () each time to such an import (due to the fact that all cross-platform methods are in a separate module). It looks like this:
cpImport('Core', 'OurProductLicense', globals()) cpImport('Formats', 'OurProductCommonFormats', globals())
Not very nice, but again, it works uniformly on both runtimes.
5. Solving the convention naming problem
The difference in the naming of methods is very simple: in C # they start with a capital, in Java - with a lowercase. What modern scripting languages ​​are convenient for is the possibility of metaprogramming. Not lisp, probably, but all the same. Ruby would be even more helpful here, but not bad either. We add cpImport ():
aliased_class = getattr(pckg, clazz)
The point here is to use setattr () to prescribe an alias for a method. In fact, for all methods starting with a lowercase letter, we create an alias right in the classroom, starting with a capital letter.
What it looks like in general. File test.py:
from cptest import isDotNet, cpUseLibrary, cpImport cpUseLibrary('Core') cpUseLibrary('Formats.Common') cpImport('Core', 'OurProductLicense', globals()) cpImport('Formats', 'OurProductCommonFormats', globals()) OurProductLicense.SetProductID(".NET Product ID" if isDotNet else "JavaProductID") OurProductCommonFormats.Initialize();
It is executed by a simple ipy test.py / jython test.py from one script without dancing with a tambourine. Which is nice.
PS Possible problems
Well, they are pretty obvious. We were lucky, our developers in the process of porting quite meticulously adhered to one convention of naming and porting names. Therefore, libraries, methods, etc., are really easy to call as described above. Otherwise, you would have to compose a table of exceptions, and take it into account in the cpImport () method. But on the whole, I liked the described approach very much and it looks like a living solution, such a rarely, probably, encountered problem.