Development/Python Unit Tests
TDF LibreOffice Document Liberation Project Community Blogs Weblate Nextcloud Redmine Ask LibreOffice Donate
This page explains the Python tests used inside of LibreOffice code. The Python based UI tests are documented on their own page.
Getting started
Python unit tests are seamlessly supported al least in the following modules :
dbaccess pyuno sw sc solenv sfx2
(You can get an up-to-date list with grep --exclude-dir="instdir" --exclude-dir="workdir" --exclude-dir=".git" --exclude-dir='autom4te.cache' -iRl "PythonTest" | grep ".mk"
)
Create an unit test
In the following, the paths are given relative to the folder containing LibreOffice sources.
Create <module>/qa/python/<my_test>.py
Example: sw/qa/python/check_field.py
# copy here a header from the file TEMPLATE.SOURCECODE.HEADER in the root of the source tree.
import unittest
from org.libreoffice.unotest import UnoInProcess
class CheckField(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._uno = UnoInProcess()
cls._uno.setUp()
@classmethod
def tearDownClass(cls):
cls._uno.tearDown()
def test(self):
xDoc = self.__class__._uno.openEmptyWriterDoc()
xBodyText = xDoc.getText()
xCursor = xBodyText.createTextCursor()
xTextField = xDoc.createInstance("com.sun.star.text.TextField.Input")
xBodyText.insertTextContent(xCursor, xTextField, True)
content = "Say hello from Python unit test!"
xTextField.setPropertyValue("Content", content)
self.assertEqual(content, xTextField.getPropertyValue("Content"))
if __name__ == '__main__':
unittest.main()
Note: For pyuno
tests, use instead pyuno/qa/pytests/<my_test>.py
Add the test to <module>/PythonTest_<module>_python.mk:
Example: sw/PythonTest_sw_python.mk:
$(eval $(call gb_PythonTest_PythonTest,sw_python))
$(eval $(call gb_PythonTest_add_modules,sw_python,$(SRCDIR)/sw/qa/python,\
...
check_field \
))
Note: For pyuno
tests, use instead pyuno/PythonTest_pyuno_pytests_<related .mk file>
For new modules
If no python unit test is defined for a module, you should create <module>/qa/python/<my_test>.py
and <module>/PythonTest_<module>_python.mk
as explained above. Then, add your test to <module>/Module_<module>.mk
.
Example: sw/Module_sw.mk
ifneq ($(DISABLE_PYTHON),TRUE)
$(eval $(call gb_Module_add_subsequentcheck_targets,sw,\
PythonTest_sw_python \
))
endif
Note: the tests defined in this way are running during subsequentcheck phase (also on Tinderboxes that activated it).
Running python unit tests
All tests of a module
make PythonTest_<...>
Examples:
make PythonTest_sw_python"
make PythonTest_pyuno_pytests_testcollections"
Just one test
PYTHON_TEST_NAME="<my_test>" make PythonTest_<...>
Debug python unit test on Linux
Prerequisites
Fedora
su -c "debuginfo-install python3"
OpenSUSE
sudo zypper in python3-base-debuginfo python3-devel-debuginfo libpython3_3m1_0-debuginfo
Note: debug repo must be activated (in yast)
make PythonTest_sw_python GDBCPPUNITTRACE="gdb --args"
Until we have this hook up and running in upstream python:
sys._breakpoint()
put this in your unit test (sw/qa/unoapi/python/get_expression.py):
import os
import signal
[...]
@unittest.expectedFailure
def test_get_expression_veto_read_only(self):
os.kill(os.getpid(), signal.SIGUSR1)
[...]
Run the test:
(gdb) run
Source the gdb python extension (needed only on OpenSUSE):
(gdb) source /usr/share/gdb/python/gdb/libpython.py
and run
(gdb) py-list
Debug python unit test on Windows
As always life on Windows is different. Debugging python unit tests is not an exception here. To debug, just put a sleep time in your python code, make sure it is say 60 seconds (If you are mst__ then 10 may be enough ;-)
import time
[...]
@unittest.expectedFailure
def test_get_expression_veto_read_only(self):
time.sleep(60)
[...]
In meantime go to MSVS and attach the debugger to python process. Be warned, that there are two python processes, wrapper and the C Python process. Obviously you need the C Python one. Once the debugger is attached, click suspend button to suspend the process execution, otherwise it just continues.
Migrate Java unit test to Python
In order to simplify our unit test setup it was decided to convert all the Java-based unit tests to be Python-based. The workflow roughly is:
- Create a new python test
- Create new make file or extend already existing one
- Add new test to this Makefile
- Remove old Java unit test.
Check: https://gerrit.libreoffice.org/4294 and https://gerrit.libreoffice.org/6017 for inspiration.
Developers who want to learn about the UNO interface (the universal interface to LibreOffice) are recommended to submit a couple of patches.
There are a couple of pitfalls when converting, this page tries to provide some common help. For specific help please talk with us on #libreoffice-dev IRC:// or mail.
Where to find the unit tests
All our tests can be found in <module>/qa/*
This is an example of a succesful patch: gerrit patch
Corresponding makefiles
There are 2 makefiles you need to modify:
<module>/JunitTest_<module>_<foo>.mk
remember to remove reference to the java file<module>/PythonTest_<module>_<foo>.mk
normally you need to add the new file in 2 places
Delete the Java file:
<module>/qa/*/<foo>.java
Add the Python file
<module>/qa/python/<foo>.py
Code translation hints
A few things are actually a lot easier to do with the Python unit tests than with the Java test framework, because:
- Python is dynamically typed
- Python tests are run in process instead of connecting from an outside process
- the Python bindings provide a deeper integration into the language than Java (they are "pythonic")
When translationg code that might lead to the wonders on "How to do this Java code in Python?", when the Java code is indeed superficial in Python -- there is no explicit code needed to do what the Java code does.
Dynamically typed (python) versus strongly typed (java) languages
You would _not_ need most of the following in dynamically typed languages like Python or StarBasic, only in statically typed languages like Java or C++.
A typical java construct:
m_xMsf = UnoRuntime.queryInterface(
XMultiServiceFactory.class,
connection.getComponentContext().getServiceManager());
Generally in UNO you are communicating by calling function over references to interfaces, not references to services or implementations.
Björn made an excellent talk at a LibreOffice conference to this theme, please watch the first 5 minutes of conference video.
Lets take the other java example:
XTextCursor xParaCrsr = xText.createTextCursor();
XTextRange xParaCrsrAsRange = UnoRuntime.queryInterface(
XTextRange.class, xParaCrsr);
xText.insertControlCharacter(xParaCrsrAsRange,
com.sun.star.text.ControlCharacter.PARAGRAPH_BREAK, false);
The first line:
XTextCursor xParaCrsr = xText.createTextCursor();
according the documentation at: doc
com::sun::star::text::XTextCursor.createTextCursor()
returns a new instance of a TextCursor service which can be used to travel in the given text context.
So this gives you a reference to the com::sun::star::text:XTextCursor
interface
of a com::sun::star::text::TextCursor
service. Note that there is a difference
between a service and an interface.
However, from: XSimpleText
we know we need to give insertControlCharacter(..) a
com::sun::star::text::XTextRange
reference as first parameter, not a
com::sun::star::text::XTextCursor
reference, which is different. We know from
the documentation of createTextCursor(..), that the
com::sun::star::text::XTextRange
reference holds a
com::sun::star::text::TextCursor
service and we can see from:
TextCursor text that the com::sun::star::text::TextCursor service extends a com::sun::star::text::TextRange service:
TextCursor text which implements a com::sun::star::text::XTextRange interface:
textrange
This can be rather confusing, but try to take it step by step then it hopefully makes sense. There are a lot of UNO Documentation and it is not always easy to match head and tail.
So:
- we have a ...text::XTextCursor
reference to a ...text::TextCursor
service
- we know that the ...text::TextCursor
service also implements the ...text::XTextRange
interface, that we need.
Thus (in Java) we need have a way to get a reference to a
com::sun::star::text::XTextRange
interface out of our reference to a
com::sun::star::text::XTextCursor
, which we know holds a
com::sun::star::text::TextCursor
and:
XTextRange xParaCrsrAsRange = UnoRuntime.queryInterface(
XTextRange.class, xParaCrsr);
does that: xParaCrsrAsRange is now a reference to a com::sun::star::text::XTextRange interface of the com::sun::star::text::XTextCursor. Thus we can call insertControlCharacter(..) with that.
xText.insertControlCharacter(xParaCrsrAsRange,
com.sun.star.text.ControlCharacter.PARAGRAPH_BREAK, false);
So how does that work in dynamically typed languages like StarBasic or Python? Well, there you just hold a reference, with no static typing attached to it. Thus you can use _any_ of the interfaces implemented by the service behind it without doing any confusing conversions. If you do:
XTextCursor xParaCrsr = xText.createTextCursor();
in Java, you can _only_ use that as a XTextCursor without conversions. If you do:
xParaCrsr = xText.createTextCursor()
in Python, you can use that as XTextCursor, XWordCursor, XSentenceCursor, XParagraphCursor, XPropertySet, XPropertyState, XMultiPropertyStates, XDocumentInsertable, XSortable. And thus for the Java code the equivalent in Python is simply:
xParaCrsr = xText.createTextCursor()
xText.insertControlCharacter(xParaCrsr, PARAGRAPH_BREAK, False)
because there is no need at all for conversion or even a xParaCrsrAsRange variable. So, in Python (or StarBasic) you never need to use queryInterface() at all!
Out-of-process UNO (Java tests) vs. In-process UNO (Python tests)
There is often a lot of boilerplate code in Java tests to connect to LibreOffice and create a test document, e.g.:
public class SomeTest
{
private static final OfficeConnection connection = new OfficeConnection();
@BeforeClass public static void setUpConnection() throws Exception {
connection.setUp();
}
@AfterClass public static void tearDownConnection()
throws InterruptedException, com.sun.star.uno.Exception
{
connection.tearDown();
}
private XMultiServiceFactory m_xMSF = null;
private XComponentContext m_xContext = null;
private XTextDocument m_xDoc = null;
@Before public void before() throws Exception
{
m_xMSF = UnoRuntime.queryInterface(
XMultiServiceFactory.class,
connection.getComponentContext().getServiceManager());
m_xContext = connection.getComponentContext();
assertNotNull("could not get component context.", m_xContext);
m_xDoc = util.WriterTools.createTextDoc(m_xMSF);
}
...
}
in Python the equivalent is simply:
class SomeTest(unittest.TestCase):
_uno = None
@classmethod
def setUpClass(cls):
cls._uno = UnoInProcess()
cls._uno.setUp()
self.m_xDoc = cls._uno.openEmptyWriterDoc()
@classmethod
def tearDownClass(cls):
cls._uno.tearDown()
Pythonic syntax simplifcations
Since commit af8143bc it is possible to use native Python language features to write code that looks more natural. For example in most cases, there is no need to write:
xSomething = xNamedAccess.getElementByName("foo")
xSomethingElse = xIndexAccess.getElementByIndex(2)
because a simple:
xSomething = xNamedAccess["foo"]
xSomethingElse = xIndexAccess[2]
does just as well. More lots of additional examples can be found in the commit message of af8143bc.
handle Sequence<PropertyValue>
Currently the python implementation only handles Sequence<any> and not Sequence<PropertyValue>, therfore a rather clumpsy workaround need to used:
p1 = PropertyValue(Name="Jupp", Value="GoodGuy")
prop1 = uno.Any("[]com.sun.star.beans.PropertyValue", (p1,))
uno.invoke(xCont, "insertByName", ("prop1", prop1))
ret = xCont["prop1"]
self.assertEqual(p1.Name, ret[0].Name)
self.assertEqual(p1.Value, ret[0].Value)
Defining env var in make file and using it in test
An env variable can be defined in make file with `gb_PythonTest_set_defs()` method. For example to define a directory to load test documents from:
$(eval $(call gb_PythonTest_set_defs,sw_python,\
TDOC="$(SRCDIR)/sw/qa/complex/writer/testdocuments" \
))
Framework's `uno.openWriterTemplateDoc()` method can load the test documents from the directory above:
def openWriterTemplateDoc(self, file):
assert(self.xContext)
smgr = self.getContext().ServiceManager
desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", self.getContext())
props = [("Hidden", True), ("ReadOnly", False), ("AsTemplate", True)]
loadProps = tuple([mkPropertyValue(name, value) for (name, value) in props])
path = os.getenv("TDOC")
if os.name == "nt":
# do not quote drive letter - it must be "X:"
url = "file:///" + path + "/" + quote(file)
else:
url = "file://" + quote(path) + "/" + quote(file)
self.xDoc = desktop.loadComponentFromURL(url, "_blank", 0, loadProps)
assert(self.xDoc)
return self.xDoc
The function can be used:
[...]
@classmethod
def setUpClass(cls):
cls._uno = UnoInProcess()
cls._uno.setUp()
cls._xDoc = cls._uno.openWriterTemplateDoc("fdo39694.ott")