Test LibreOffice automatically

Signs of Life at the Office

© Lead Image © Alexander Zelnitskiy, 123RF.com

© Lead Image © Alexander Zelnitskiy, 123RF.com

Article from Issue 187/2016
Author(s):

Companies that depend on LibreOffice have a reason to wonder whether the office suite is working on all systems. You can use Python and the LibreOffice API to check.

A reliable office suite is one of the key applications in many environments, so users will quickly notice if the suite won't start or individual functions fail after an update. However, it's not just after an Office update that the heart of modern offices can stumble – renewal of operating system components also appears to be a point where established workflows experience issues.

If your network uses a specific set of templates, macros, and forms again and again, in practice, it is enough to at least test the key documents once before rolling out the update in all departments.

Automating the Test

What is even more helpful than the test itself is its automation – at least for administrators. Given that manually clicking through the various types of documents before each update is both boring and error-prone.

LibreOffice offers a whole range of options to control the software via the application API via script. The most common variant is probably to control it using macros from LibreOffice itself (also thank to other known Office suites). The Office suites for writing these macros may usually offer a basic dialect, but – after performing a little setup work – LibreOffice also lets you formulate these macros in Python.

LibreOffice can be controlled just as well remotely – such as from an external process or from a Python script. The same access to the LibreOffice API is available as in the macro environment.

Administrators therefore have the option to test LibreOffice on a network computer without having to open or manually operate LibreOffice. This means, for example, that admins can run a quick automated check before rolling out a new version to make sure LibreOffice will continue to work as desired after the update.

Administrators or developers can also use the same method to conveniently automate document processes, to create form letters or PDF documents, to address LibreOffice behind a web front end, or much more.

The Games Begin

The first step in setting up LibreOffice testing is to install the libreoffice-script-provider-python package. However, this package is essentially a meta package that mainly deals with ensuring LibreOffice and the Python package python3-uno are installed.

On distributions where there is no package called libreoffice-script-provider-python, it should be enough to install the LibreOffice packages and the python-uno, python3-uno, or python2-uno package – depending on the Python version used. The script developed here is based on Python 3.

On Mac OS X, the LibreOffice package contains its own Python interpreter under /Applications/LibreOffice.app/Contents/MacOS/python. It also has the uno package, meaning you can start with scripting on Mac OS X right away.

The Python interpreter certainly readily informs you if access to the LibreOffice interface is possible. The >>> import uno import must not fail after starting using the command python3 or under Mac OS /Applications/LibreOffice.app/Contents/MacOS/python.

If this Python line returns an Import Error, either the uno package is missing or it is not in the expected path for Python libraries. However, if Python is able to import the package, there should be no problem accessing LibreOffice.

Stimulating LibreOffice

Before you can control LibreOffice remotely, you need to restart the application and prompt it to listen to remote commands. The soffice binary needs the slightly elongated option for this:

--accept=socket,host=localhost,port=8100,tcpNoDelay=1;urp;

The option value indicates what LibreOffice is doing now. The application then accepts (accept=...) incoming requests on the open socket (socket) listening to port 8100 (port=8100) – at least, when the requests come from its own computer (host=localhost).

The tcpNoDelay=1 option is recommended for sending network packages immediately – even those with minimal amounts of data. Many remote control commands for LibreOffice are very short, and you'll want an immediate response. Without tcpNoDelay=1, the network layer waits to see whether yet more data needs to be sent via the connection.

The final addition urp; signalizes to LibreOffice that the communication should use UNO Remote Protocol. This is the standard protocol used by LibreOffice for communication. UN stands for "Universal Network Object" and corresponds to the name of the Python package that you initially needs to install to give Python the opportunity to communicate with LibreOffice.

In addition to the open port, it is a good idea to use the --headless option to get LibreOffice to dispense with all displays when starting the application and not to allow any further user input. This means it is only possible to control the application using the open port and it responds more quickly to the script commands. However, if you make changes to the script, you should comment out this option for logical reasons.

Then you can observe what exactly LibreOffice does. The complete call looks like this:

soffice '--accept=socket,host=localhost,port=8100,tcpNoDelay=1;urp;' --headless

The accept option on the shell belongs in single quotes so that the semicolons are passed on to LibreOffice uninterpreted.

As it would be inconvenient to start LibreOffice manually before a test run, there is nothing to prevent the Office application being opened from the Python script. That is what lines 13 to 27 from Listing 1 do. The parameters from the socket connections are stored in the script in the SOCKET variable because, on one hand, these values are passed to LibreOffice as an option at start up and, on the other, are also required if the script is supposed to establish the connection to the LibreOffice Socket.

Listing 1

Controlling LibreOffice Remotely

001 import filecmp
002 import sys
003 import uno
004 from com.sun.star.beans import PropertyValue
005 from com.sun.star.connection import NoConnectException
006 from glob import glob
007 from os import mkdir, path
008 from shutil import rmtree
009 from subprocess import Popen
010 from tempfile import NamedTemporaryFile
011 from time import sleep
012
013 SOCKET = 'socket,host=localhost,port=8100,tcpNoDelay=1;urp;'
014 exitCode = 0
015
016 # Starting LibreOffice
017 try:
018     app = Popen([
019        '/usr/lib/libreoffice/program/soffice',
020         '--headless',
021         '--accept=' + SOCKET
022     ])
023 except Exception as e:
024     raise Exception("It was not possible to start LibreOffice: %s" % e.message)
025
026 if app.pid <= 0:
027     raise Exception('It was not possible to start LibreOffice!')
028
029 # Establishing the connection to LibreOffice
030 context = uno.getComponentContext()
031 resolver = context.ServiceManager.createInstanceWithContext(
032     'com.sun.star.bridge.UnoUrlResolver',
033     context
034 )
035
036 n = 0
037 while n < 12:
038     try:
039         context = resolver.resolve(
040             'uno:' + SOCKET + 'StarOffice.ComponentContext'
041         )
042         break
043     except NoConnectException:
044         pass
045     sleep(0.5)
046     n += 1
047
048 desktop = context.ServiceManager.createInstanceWithContext(
049     'com.sun.star.frame.Desktop',
050     context
051 )
052
053 if not (desktop):
054     raise Exception('It was not possible to generate the LibreOffice desktop!')
055
056 # LibreOffice is controlled via the connection from here
057
058 # Inital "Hello world" test
059 document = desktop.loadComponentFromURL(
060     'private:factory/swriter',
061     '_blank',
062     0,
063     ()
064 )
065 cursor = document.Text.createTextCursor()
066 cursor.ParaStyleName = 'Heading 1'
067 document.Text.insertString(cursor, 'Hello world!', 0)
068
069 txtFile = NamedTemporaryFile('w+', encoding='utf-8-sig')
070
071 textFilter = PropertyValue()
072 textFilter.Name = 'FilterName'
073 textFilter.Value = 'Text'
074
075 document.storeToURL('file://' + txtFile.name, (textFilter,))
076
077 text = txtFile.read()
078 if (text != 'Hello world!\n'):
079     print("FAIL: \"Hello world\"-test failed! The result was: %s" % text)
080     exitCode = 1
081 else:
082     print('OK: "Hello world!" is exported as expected.')
083
084 document.close(False)
085
086 # Preparing the test and result folder
087 if sys.argv[1] and path.isdir(sys.argv[1]):
088     testdir = path.abspath(sys.argv[1])
089     resultdir = path.join(testdir, 'Result_files')
090     if path.isdir(resultdir):
091         rmtree(resultdir)
092     mkdir(resultdir, 0o700)
093
094     # Run through of the test documents
095     pdfFilter = PropertyValue()
096     pdfFilter.Name = 'FilterName'
097     pdfFilter.Value = 'writer_pdf_Export'
098
099     testfiles = glob(path.join(testdir, 'Test_*'))
100     for testfile in testfiles:
101
102         document = desktop.loadComponentFromURL(
103             'file://' + testfile,
104             '_blank',
105             0,
106             ()
107         )
108
109         resultFile =  path.basename(testfile)[5:-4]
110
111         fullPath = 'file://' + path.join(resultdir, 'PDF_' + resultFile + '.pdf')
112         document.storeToURL(fullPath, (pdfFilter,))
113
114         textResultFile = path.join(resultdir, 'Text_' + resultFile + '.txt')
115         fullPath = 'file://' + textResultFile
116         document.storeToURL(fullPath, (textFilter,))
117
118         expectationFile = path.join(testdir, 'Text_' + resultFile + '.txt')
119         if (path.isfile(expectationFile)):
120             if filecmp.cmp(textResultFile, expectationFile, False):
121                 print("OK: File %s is exported as expected." % testfile)
122             else:
123                 print("FAIL: File %s is not exported as expected!" % testfile)
124                 exitCode = 1
125         else:
126             print("--: No comparison file for %s." % testfile)
127         document.close(False)
128
129
130 # Closing LibreOffice
131 desktop.terminate()
132
133 sleep(2)
134
135 app.wait()
136
137 sys.exit(exitCode)

Ultimately, the Python script doesn't run anything at the start other than the soffice call described above from the command line. The script uses the subprocess.Popen class for this in line 18. And, soffice responds itself with the full path.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • LibreOffice Macros with ScriptForge

    ScriptForge helps you automate LibreOffice by building portable macros.

  • Text to Speech

    Visually impaired users often find working with text and tables in office suites difficult. Pico TTS, a text-to-speech synthesizer, and the Read Text extension for LibreOffice and OpenOffice provide a solution.

  • LibreOffice 7 Now Available

    The LibreOffice 7 office suite is now available with important compatibility improvements.

  • The Clear Choice

    While LibreOffice and OpenOffice have a shared past, LibreOffice outstrips OpenOffice in contributors, code commits, and features.

  • LibreOffice 3.5

    PowerPoint shapes. We investigate how the two office modules work together.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News