Developers Club geek daily blog

2 years, 7 months ago
I think everything it is aware about advantage of autotests. They help to keep a code in operating state even at essential changes. Also it can relieve testers of tiresome handwork and allows to concentrate on more interesting types of testing.

In spite of the fact that to separate parts of our project more than 25 years, we only at the very beginning of a way of implementation of automatic testing. Nevertheless, we already have some progress of which I want to tell in this article.

How to write good autotests – a subject of separate article. And probably not one. I will tell you as we implemented testing of separate components. Components are written on With ++ and very similar to SOM have interfaces. As language for tests we selected python and we use very powerful test framework of PyTest. In article I will tell about difficulties of a linking of C++/COM and a python, reefs which we came across and as solved these problems.


  • I cannot kopipastit just like that a code of our project because of NDA. Therefore all examples in article are written from scratch and were never compiled. Therefore there can be small inaccuracies, syntax errors or non-compliance with rules of a design of a code. But I tried to convey the main meaning.
  • I am not an expert in a python. To tell the truth, the python I began to learn approximately in the middle of work on the project. Therefore some statements concerning a python can be or not absolutely correct, or are worked not up to the end out.
  • The Pitonovsky code in examples can not correspond to pep8 since often reflects the Somovsky prototype and borrows its stylistics.
  • This article not a manual on cython'u, many things stayed behind scenes


In the project on which I work we develop the big and difficult module. Several million code lines on With ++, ten large components and one hundred dll under a cowl. This module is used in several huge applications.

Historically so it developed that all this is tested only through UI, and mostly manually. Often so it turns out that change in one area gets out a bug somewhere in other place. Also find this bug only in several days, or even weeks. And sometimes something emerges even in several months when other product decides to integrate the new version of our module.

We have a unit tests which chase on CI on kommita. But the code was more than 15 years old when at us started talking about TDD. A code monolithic and just like that to start it separately will not leave. Big refactoring on which nobody will give resources is necessary. Therefore tests we have a unit only on simple functions or separate classes.

But if the module has some API, and a testitis this module it is possible through this API. It is possible to collect yuzkeysa of all applications which us use and to write on it autotests. Then it would be possible to drive these tests directly for CI on kommita. So the idea of component testing was born.

But here who will write tests? Testers could think up good test cases and prepare test data, but testers do not know With ++ (and who know those fast dump in developers). Programmers could zakodit such tests, but usually the imagination is enough only for couple of positive scenarios. To cover all negative cases, usually, there is not enough patience.

We decided to adopt experience of colleagues from the next command. They for the component made vrapper by means of cython and exposed the simplified interface which is used for tests in a python. The threshold of entry in a python is much lower than in With ++. Testers can master easily a python for couple of days and begin to write good autotests.

What does it have to do with SOM?

Before beginning to describe our tortures it is necessary to tell a couple of words about our interfaces. We use the technology which is pinched with SOM and ported on Linux, poppy and fryu. There are some infrastructure distinctions connected with lack of the register, but for article it not essentially.

SOM-opodobnaya the technology gives us a heap of buns as that ready plaginno-component infrastructure. We can easily join the modules written by different commands in the different countries (including and third party plug-ins). At the same time we do not take a steam bath questions of compatibility of different compilers, rantaym and standard libraries. Also the stylistics of interfaces of all modules, agreements on parameter passing and returned values, lifetime of objects – all this are regulated by agreements as with SOM'E.

There is also a back. In modules we can use any buns of modern standards C ++. But in public interfaces we have to follow rules SOM'A – only simple types or IUnknown interfaces successors. Any STL. Any eksepshen, only HRESULT. Because of it the code on borders of modules turns out very bulky and not strongly readable.

The first experience with cython

For a start we defined ten interfaces of our module by means of which it would be possible to implement small, but complete workflow.

But these interfaces though are part of public API, in practice they are rather low-level. To perform a certain operation it is impossible to take and cause single function or a method just like that. It is necessary to create heels of objects, to connect them with each other, to start on execution and to wait for result through future. All this complexity is necessary to organize a tranzaktsionnost, asynchrony, Undo/Redo, to organize access to potokonebezopasny interiors and still heaps of other things. Well, 2 screens of a code on With ++ in SOM style.

We decided to follow the recommendation of our colleagues and to write a small layer which would hide low-level interfaces. In a python it was offered to expose several high-level functions.

Vrapper on cython just forwarded challenges in with ++:
cdef class MyModuleObject():
	cdef CMyModuleObject * thisptr	# wrapped C++ object

	def __init__(self):
		self.thisptr = new CMyModuleObject()

	def __dealloc__(self):
		del self.thisptr

	def DoSomething1(self):

	def DoSomething2(self):

	def GetResult(self):
		return self.thisptr.GetResult()

C ++ implementation of the class CMyModuleObject was already engaged in useful effects: created objects of our module and called at them some useful methods (those 2 screens of a code).

Cython is in fact the translator. On the basis of the source code above cython generates ton of a sishny code. If to compile it as dll/so (and to rename into pyd), then we will receive the pitonovsky module. With ++ implementation of CMyModuleObject the dl needs also to lodge in this. Now our pitonovsky module can be imported from a python (prodolbavshis at first with ways of import). It is possible to start by means of the normal pitonovsky interpreter, the main thing what the architecture would match. Executing a line of import, the python itself will lift our dll, will initialize and proimportirut everything that is necessary.

The script on a python looked as that so:
from my_module import *
obj1 = MyModuleObject()
print obj1.GetResult()

It is cool! Much simpler, than on With ++!

At the first stage decided not to bother with test frameworks, and at first to fulfill approach on normal scripts. In such type it is already possible to write something, But this approach did not differ in flexibility. If it was necessary to change something in a layer, then it was necessary to change a code on With ++.

Vrapim COM interfaces

I insisted on trying to zavrapit directly low-level interfaces, and a layer if it is necessary to write on a python. The idea was sold to the administration that we can zavrapit interfaces 1 to 1 and test at the level of our public API.

No sooner said than done. We quickly came to such scheme. The designer of pitonovsky object creates Somovsky object and owns the link to it. Links, of course, are considered as the smartpointerom-copy of CComPtr.

cdef class PyComponent:
	cdef CComPtr[IComponent] thisptr

	def __cinit__(self):
		# Get the COM host
		cdef CComPtr[IComHost] com_host
		result = GetCOMHost(IID_IComHost, <IUnknown**>&(com_host))
		hresultcheck (result)

		# Create an instance of the component
		result = com_host.inArg().CoCreateInstance(
				IID_IComponent, <void**>self.thisptr.outArg()  )
		hresultcheck( result )

	def SomeMethodWithParam(self, param):
		result = self.thisptr.inArg().SomeMethodWithParam(param)
		hresultcheck (result)

	def GetStringFromComponent(self):
		cdef char [1024] buf
		result = self.thisptr.inArg().GetStringFromComponent(buf, sizeof(buf)-1)
		return string (buf)

HRESULT of functions to us is normal it is uninteresting. If function is successful – well and is nice. If it zafeylitsya, then most likely it is not necessary to go further. Therefore we simply check an error code and we throw a pitonovsky exception. Processing of return codes is not taken out on the level of a client pitonovsky code that does a client code significantly more compactly and more readably.

class HRESULT_EXCEPTION(Exception):
	def __init__(self, result):
		super(HRESULT_EXCEPTION, self).__init__("Exception code: " + str(hex(result &0xffffffff)))

cpdef hresultcheck(HRESULT result):
	if result != S_OK:
		raise HRESULT_EXCEPTION(result)

Pay attention that the hresultcheck function is declared as cpdef. It means that it can be caused as pitonovsky (sometimes hresult at us is checked in a python), and native sishny. The second property significantly reduces the processing code of errors generated sitony and accelerates execution. We did not master SUCCEEDED macro challenge therefore we compare to S_OK – so far is enough.

Sometimes all of us departed from a vrapping 1 to 1 when it was clear that certain interfaces and their methods need to be used only by one certain method and in any way differently. For example, if it is meant that SOM object will be created empty, and then in it parameters through Set * will be stuffed () methods or a challenge of any Initialize (), in this case at the level of a python we did just convenient designer with parameters.

Or here still example. Happens that the request to one object conceptually returns the link to other object (or just new object). In SOM it is necessary to use output parameters, but in a python it is possible to return object properly.

cdef class Class2:
	cdef CComPtr[IClass2] thisptr
cdef class Class1:
	cdef CComPtr[IClass1] thisptr

	def GetClass2 (self):
		class2 = Class2()
		result = self.thisptr.inArg().GetClass2( class2.thisptr.outArg() )
		hresultcheck ( result )
		return class2

From the point of view of encapsulation a code not really good – one object gets into guts of another. But in a python with encapsulation (more precisely with a privacy) and it is so not really good. But we did not think up more beautiful method yet. There is a risk that somebody will try to create Class2 hands in a client code, nothing good, probably, will leave. I will be glad if who prompts option of the private designer in a python.

Code samples are located in files with the pyx expansion above (they, by the way, can be done much, but not to push everything in one). It as cpp in pluses – the file with implementation. But in the sitena the file with declarations – pxd – the place where all names which to consider sishny will be described is still necessary.
from libcpp cimport bool

from libcpp.vector cimport vector
from libcpp.string cimport string
from libc.stdlib cimport malloc, free

cdef extern from "mytypes.h":
	ctypedef unsigned short int myUInt16
	ctypedef unsigned long int  myUInt32
	ctypedef myUInt32 HRESULT
	ctypedef struct GUID:
	ctypedef enum myBool:

cdef extern from "hresult.h":

cdef extern from "Iunknown.h":
	cdef cppclass IUnknown:
		HRESULT QueryInterface (const IID &iid, void ** ppOut)
		HRESULT AddRef ()
		HRESULT Release ()

cdef extern from "CComPtr.h":
	cdef cppclass CComPtr [T]:
		# this is trick, to assign pointer into wrapper
		T&assign "operator="(T*) 
		T* inArg()
		T** outArg()

cdef extern from "comhost.h":
	cdef extern IID IID_IComHost
	cdef cppclass IComHost(IUnknown):
		HRESULT CoCreateInstance ( const GUID&classid,
        IUnknown* pUnkOuter, 
        const IID&iid, 
        void** x   )

Pay attention to CComPtr:: operator= (). If in a sitonovsky code to try to appropriate directly Ccomptr'U – nothing will leave. It will not be able just to sort this syntactic construction plainly. It was necessary to resort to a trick of a pereimenovyvaniye of characters. So assign this how the character will be look in the sitena, and is in quotes set that needs to be caused in a sishny code.

The trick is useful if it is necessary to call a pitonovsky class or function in the same way as well as sishny.

cdef extern from "MyFunc.h":
	int CMyFunc "MyFunc" ()

def MyFunc():

Returning to our project. The Pitonovsky code though is simpler and more compact, but still too low-level for most of users. Therefore we after all decided to leave a layer, having rewritten it on a python. As a result those 2 pages of a bulky COM code at us turned into it

def do_operation(param1, param2):
	operation = DoSomethingOperation(param1, param2)
	engine = TransactionEngine()
	future = engine.Submit(operation)
	return future.GetResult()

So our code became much more compact and more clear, it was possible to use as high-level do_operation interfaces (), and if necessary to go down to "sishny" interfaces.

There was a feeling of flexibility, it was not necessary to recompile every time With ++ part. Moreover, for start we needed to zavrapit only 10 interfaces, and for each subsequent feature it was necessary to dovrapit only 1-2 – it really added forces and belief in the selected approach.

Problems begin

In such type the technology can already be suitable for the majority of projects, but we rested against several fundamental restrictions.

So, our COM a host (object which provides infrastructure of SOM everyone there CoCreateInstance and other) is normal plus object. So, someone has to create it (analog of CoInitialize) and then to delete (CoFinalize). But here in what a problem, the pitonovsky module has no main (). In any case, in that type as it was necessary for us.

Therefore we created plus object of Application and put initialization / finalizatsiyu a host in this object. Wrote a vraper who allowed us to create this object from a python (at the beginning of each script).

But very quickly we began to catch kresh on an output. It turned out that unlike With ++ (the first created object will be removed the last) the order of destruction of objects in a python is not defined. Well, in any case, there is no opportunity to influence it. Depending on a moon phase the python beat object of Application the first, that extinguished COM infrastructure and compulsorily unloaded all components. Then the python deleted some other object at which the link to some Somovsky object was available. Attempt to cause Release () from dl which was already unloaded brought to I christen.

The second problem – an arrangement of the performed file. It turned out that in our big component there are a lot of places which try to open files with data on some way concerning the performed file. It is normal if the final application is installed in system by some known method. It is also normal if we work with the application collected in the current directory. But such method ceases to work if the python in a system directory becomes the performed file suddenly.

There was even a function which allows to redefine an application directory. It worked in most cases. But, unfortunately, there were velosipedostroitel who ignored this redefinition and continued to calculate a way independently concerning an ekzeshnik. It would be correct to take it yes to repair, but it would demand considerable labor costs. Decided that it will roll about in a bekloga so far.

At last the third problem – event loop. The matter is that our module is very difficult and interactive. It is not just library in style "cause function-receive result". It is the huge combine. Inside couple of hundreds of flows which as that communicate turn. Some parts of a code were written to times of the Mesozoic and intended for execution only in the main flow (otherwise will not work). In other places there is a sending of the message hardkody in the main flow, expecting that there know how to process this message. And still we have own subsystem of flows and messages which also means that in the main flow it will be obligatory to turn an operation cycle of messages and all this to conduct. Without it in any way.

The simplest solution on start was to insert into our class Application the run_event_loop method () which twisted a cycle of messages. Process stopped when our useful work came to the end (as I understand it now occurred on pure coincidence :))

Generally, scripts like it normally worked for us: we start some work with the help of non-blocking function (which does not wait for the termination) then we steep in ivent magnifying glasses
app = Application()

But here with scenarios which demanded some interactivity the problem turned out. We could not start, for example, work, and then in couple of seconds try to stop it. Upon work did not begin, the operation cycle of messages in the main flow was not started yet. And if the cycle was started, then in a python we do not return any more.

Of course, it would be possible to fence something asynchronous and at the level of a python, but it is explicit not that it would be desirable. Approach was supposed to be pushed to people who are not tempted with asynchronous systems. They would like to write just here not to take a steam bath some ivent with magnifying glasses

Without thinking twice we tried to start processing in other flow, and in the main thing to twist a cycle of messages. But we right there rested against the following problem – GIL (Global Interpreter Lock). It turned out that pitonovsky flows actually are not executed in parallel. In each timepoint only one flow works, and flows switch each 100 commands. All this regulates this GIL, stopping all flows except one.

It turned out that if the main flow went to the app.run_event_loop function () and did not return (and on design it has to hang there), then other pitonovsky commands in other flows are not executed. Just in the main flow 100 more teams were not made, and the interpreter considered that to switch so far early.

The solution was found in a key word of a siton of nogil. The piece of a code the marked nogil releases GIL in the beginning, then executes a sishny challenge. At the end of GIL it is taken again. Thus the main flow released GIL and went to a cycle of messages. The second flow received management and did there everything that is necessary.
def Func(self):
	result = 0
	cdef IComponent * component = self.thisptr.inArg()
	with nogil:
		result = component.Func()

Cython, by the way, piece very whimsical. It not always allows to stir a pitonovsky and sishny code in one line. Also it does not allow to cause some pitonovsky constructions and to create new variables in nogil sections (logically, access to guts of a python which just and are protected by Gil'om is for this purpose necessary). It is necessary to be perverted here and so correctly to declare variables and to do the necessary challenges.

In total it seems as began to work, but it is very unstable. We constantly caught some kresh and podvisaniye, and also constantly came across idle functionality (the file on a relative path did not open, but nobody threw an error).

Design on the contrary

We tried to win several weeks against these 3 problems, tried different approaches. But every time there was the next unsoluble problem. Most of all delivered to GIL and how to win against unloading of COM of a host we did not represent at all.

We even thought to jump off on lua, but some of restrictions all the same would remain. Only at the same time still it should pass all way from scratch.

But here the daring idea came. And what if we start not a sishny code from a python, and on the contrary a python from sishny? Let's write the application which will do the following:
  • To initialize SOM a host
  • To start the second flow
  • In the main flow to start event loop
  • In the second flow to start the interpreter of a python (it has corresponding API for embedding in other applications)

This approach solves at one stroke all 3 problems:
  1. We control lifetime of SOM of a host and we can guarantee its destruction after the pitonovsky flow finishes the work.
  2. Our test application will live near the main product, so, all relative paths will work.
  3. At last, any problems with Gil'om. The python executes a one-line script, so it is not necessary to share resources with anybody.

Also you know? This approach worked! There were, however, several minor problems which managed to be solved over time
  • some challenges have to be executed, after all, in the main flow. Well nothing, we are zapulyal the message in the main flow, with a request to execute what is necessary.
  • It was necessary to tinker with the PYTHON_HOME and PYTHON_PATH installation fairly. The nontrivial moment was that neither the pitonovsky Py_SetPythonHome function (), nor standard setenv () do not copy the transferred line to itself, and just remember the pointer. In our case it was the pointer on temporary variable.
  • not to depend on the version of a python, decided to carry all giblets with themselves. Including standard library (which as it appeared, is perfectly read directly from zip'a) and several alternative libraries

One more problem with which it was necessary to tinker – the sys.exit function (). It was necessary to us to catch return code from unittest and to transfer to an output then then to process on CI.

It works so. If someone in a script causes sys.exit (), actually SystemExit an exception is generated. This exception is caught by a python and, as well as any other exception caught globally has to be printed in the console together with a stack treysy. But the Py_PrintEx function knows that there is such special case and if to us suggest to print SystemExit exception, means it is necessary to cause sishny exit ()

Yes, here so! Function with the name Print does exit challenge (). And this exit fairly fulfills – just takes and cuts down all application. And he wanted to spit on the fact that in the application there are not released hendla, incomplete flows, open files, not finalized modules, one million active flows and so on.

But python (in any case 2.7.6. Second-hand articles, I know) does not allow to obrulit it at the level of API. Just it was necessary to copy to itself in the project several functions from source codes of a python (since PyRun_SimpleFileExFlags () and several private which it causes) and were drunk up under themselves. So, our version in case of SystemExit correctly appears and returns return code. Thus the test application after end of pitonovsky part can correctly clean and extinguish itself.

First we had 2 proyektnik – one bildit the test application with the built-in python, and the second, as before, the loaded module for a python. But later we integrated all this in one proyektnik. The test application initialized a python then caused function of initialization of our pitonovsky module (it is generated sitony). Thus the python already on start already knew about our module (though it was necessary to do imports all the same).


In such type the test application very well showed itself. We fastened a test framework (standard unittest) and testers began to write tests gradually. Ourselves continued to vrapit interfaces meanwhile.

Fastening the next piece of functionality we came across that in certain cases we need to be able to accept kollbek. I.e. the python synchronously causes function, and that in the subsoil causes kollbek in a python.

The plus interface looks so:
class ICallback : public IUnknown
	virtual HRESULT CallbackFunc() = 0;
Class IComponent : public IUnknown
	virtual HRESULT MethodWithCallback(ICallback * cb) = 0;

Pitonovsky class in any sauce it will not be able to otnasledovatsya from the plus interface. Therefore in sishny part of the project it was necessary to make the implementation which threw challenges in a python.

//Forward declaration
struct _object;
typedef struct _object PyObject;

class CCallback : public ICallback
	//COM stuff

	CCallback * Create();
	// ICallback
	virtual HRESULT CallbackFunc();
	void SetPythonCallbackObject(PyObject * callback_handler);

	PyObject * m_pPythonCallbackObject;

const char PythonMethodName[] = "PythonCallbackMethod";

void CCallback::SetPythonCallbackObject(PyObject * callback_handler)
	// Do not addref to avoid cyclic dependency
	m_pPythonCallbackObject = callback_handler;

HRESULT CCallback::CallbackFunc()
		return S_OK;

	// Acquire GIL
	PyGILState_STATE gstate = PyGILState_Ensure();
	if ( gstate == PyGILState_UNLOCKED )
		// Call the python method
		char * methodName = const_cast<char *>(PythonMethodName); //Py_Api doesn't work with constant char *
		PyObject * ret = PyObject_CallMethod(m_pPythonCallbackObject, methodName, NULL);
		if (!ret)
			if (PyErr_Occurred())
			std::cout<<"cannot call"<<PythonMethodName<<std::endl;

	// Release the GIL
	return S_OK;

The special moment in this code – capture of GIL. Otherwise at best the python will break on check that GIL is taken, but most likely either will hang, or skreshitsya.

We have a console application therefore error output in the console it. Even if the pitonovsky code will throw out an exception our processor will catch it and will print treysbek.

From a siton it looks so:
cdef class PyCallback (object):
	cdef CComPtr[ICallback] callback

	def __cinit__(self):
		self.callback.assign( CCallback.Create() )
		self.callback.inArg().SetPythonCallbackObject(<PyObject *> self)

	def PythonCallbackMethod(self):
		print "PythonCallbackMethod called"


cdef class Component:
	cdef CComPtr[IComponent] thisptr

	def __cinit__(self):
		// Create IComponent instance
	def CallMethodWithCallback(self, PyCallback callback):
		cdef IComponent * component = self.thisptr.inArg()
		cdef ICallback * cb = callback.callback.inArg()
		hresult = 0
		with nogil:
			hresult = component.MethodWithCallback(cb)

At MethodWithCallback method call () it is not necessary to release GIL, otherwise kollbek will be able to take it.

With a client pitonovsky code already everything has to be simply and clear
component = Component()
callback = PyCallback()

Too it is not difficult to organize parameters in kollbeka, but we so far still had not to do it. I think, there it will be necessary to conjure a little over pitonovsky API, or to spot what code generates cython.

Pleasant surprise was the fact that such code is able to fulfill even kollbek from other flows at asynchronous operations. For example, the pitonovsky code starts some asynchronous operation then management returns in a python in our component. After a while from other flow is caused kollbek, it takes GIL (sometimes it is necessary to wait until the pitonovsky flow releases it) then the control is transferred to a pitonovsky code of a kollbek. At this PyGILState_Ensure () itself will understand that it was caused from another, a flow unfamiliar to it, and will create an internal pitonovsky context of a flow.

The only complexity, however, appeared to settle a situation when kollbek comes at the end of work of a script. In some situations happened so that the pitonovsky script ended, the python begins to clean the interiors and to delete objects, and here flies kollbek from other flow. Usually it came to an end kreshy.

We acted this way. In a sishny code which causes pitonovsky object, added the global counter of active kollbek. It was necessary in our PyRun_FileExFlags version () which we already stole by then from source codes of a python, before PyArena_Free challenge () stuck such piece
	PyThreadState *_save = PyEval_SaveThread();
	while (GetCurrentlyActiveCallbacks() > 0)
		; // semicolon here is correct


Sandwich python-> cython-> C ++ it was very successful as a framework for API of autotests. The threshold of entry in a python is very small, in comparison with other programming languages. Any competent tester for couple of days will master a python at the level sufficient for writing of autotests. In this case to think up the main thing as it is possible to test this or that functionality, and to express it in a code – a trick.

We managed to construct a layer, convenient for use, which in a python exposes the simple and clear interface. Challenges of low-level C++/COM of a code are hidden for vraper and the understanding of this code is not necessary for the majority of tasks. But in case of need there it is easy to go down.

Recently we fastened PyTest as a framework for writing of tests. Very much drives! Tests became even simpler, more clear and quicker.

Now any serious architectural shortcomings are not visible, but some bugs still are. So, for example, now there is couple of cyclic dependences because of what several key objects do not want to collapse in any way. But where it is correct to break off cyclic dependence we did not think up yet.

As for the siton. The cython'a developers make contact and already repaired for us a couple of bugs. Releases at them leave regularly.

Cython piece rather whimsical. Not always intelligibly explains that exactly it is not right in the source file, and at times also specifies not there. We did not master some things.

We also fastened an interactive mode of a python. It is sometimes more convenient to debug a pitonovsky code directly in the console of a python, than to govern a script and to start it again and again. There was everything simply – to cause it enough:
PyRun_InteractiveLoop(stdin, "<stdin>");

Developers With ++ the module actively start tests directly from IDE. It is possible just to take the test which was filled up on CI and to otdebazhit. All bryak in With ++ a code work as it is necessary. But here is how to debazhit pitonovsky part we for the present did not think up. However, there was no special need — with PyTest tests turn out very simple and there are nothing to debazhit there.

Now understanding that the technology took place came. It is easy and convenient to expand functionality. The people liked to write autotests though so far are suspicious.

I hope, article will be useful, and you will be able to organize at yourself something similar too.

This article is a translation of the original post at
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here:

We believe that the knowledge, which is available at the most popular Russian IT blog, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus