Development/Python Unit Tests

From The Document Foundation Wiki
Jump to: navigation, search

This page explains python tests inside of the LibreOffice code as internal tests. The python based UI tests are document on an own page.

Getting started

Python unit tests are seamlessly supported: create an unit test, create make file and include the new make file into the module make file. Create $(SRCDIR)/sw/qa/python/check_field.py

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()

Add a new make file 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 \
))

Include it into the existing Module_sw.mk make file:

ifneq ($(DISABLE_PYTHON),TRUE)
$(eval $(call gb_Module_add_subsequentcheck_targets,sw,\
    PythonTest_sw_python \
))
endif

Run python unit test

make PythonTest_sw_python

Note: the tests defined in this way are running during subsequentcheck phase (also on Tinderboxes that activated it).

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 freenode #libreoffice-dev connect via webchat 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