Development/Python Unit Tests

    From The Document Foundation Wiki

    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:

    1. Create a new python test
    2. Create new make file or extend already existing one
    3. Add new test to this Makefile
    4. 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 are provide a deeper integration into the language that 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")

    Additional links