Aus Linux-Magazin 05/2016

Libre Office automatisiert testen

© vasin leenanuruksa, 123RF

Dass Libre Office auf allen PCs stets funktioniert, ist in vielen Firmen lebenswichtig. Zur automatisierten funktionellen Anamnese reichen Python und das Libre-Office-API.

Vielerorts ist die Office-Suite eine der zentralen Anwendungen – umso größer gerät der Schock, wenn nach Software-Updates oder -Upgrades die Suite nicht mehr startet oder einzelne Funktionen ausfallen. Ins Stolpern kann das Herz moderner Büros nicht nur nach einem Office-Update selbst geraten, jede Erneuerung einer Betriebssystem-Komponente erscheint geeignet, die etablierten Arbeitsabläufe zu stören.

Betreut der Administrator Abteilungen, die einen Satz bestimmter Vorlagen, Makros und Formulare immer wieder verwenden, genügt es in der Praxis, zumindest die zentralen Dokumente ein Mal zu testen, bevor der Admin das Update in alle Abteilungen ausrollt.

Test automatisieren

Noch hilfreicher als der Test selbst ist seine Automatisierung – zumindest für den Administrator. Denn dass dieser sich vor jedem Update manuell durch die verschiedenen Dokumenttypen klickt, ist nicht nur langweilig, sondern auch fehleranfällig.

Libre Office bietet eine ganze Reihe von Möglichkeiten an, um die Software über das API der Anwendung per Skript zu steuern. Die bekannteste Variante dürfte dabei – auch aufgrund anderer bekannter Office-Suiten – die Steuerung über Makros aus Libre Office selbst heraus sein. Üblicherweise bieten die Office-Suiten für das Schreiben dieser Makros zwar einen Basic-Dialekt an, aber nach ein wenig Einrichtungsarbeit erlaubt es Libre Office, diese Makros auch in Python zu formulieren.

Die Steuerung von Libre Office funktioniert genauso gut von außen – etwa aus einem externen Prozess oder auch aus einem Python-Skript heraus. Hier steht der gleiche Zugriff auf das Libre-Office-API zur Verfügung wie innerhalb der Makro-Umgebung.

Dem Admin bietet sich über diesen Weg eine Möglichkeit zum Testen, ohne dafür Libre Office selbst öffnen und manuell bedienen zu müssen. So kann er zum Beispiel vor dem Ausrollen einer neuen Version einen kurzen automatisierten Check fahren, der absichert, dass Libre Office auch nach dem Update weiterhin so funktioniert wie gewünscht.

Die gleiche Methode können Administratoren oder Entwickler auch verwenden, um komfortabel Dokumentenabläufe zu automatisieren, Serienbriefe oder PDF-Dokumente zu erstellen, Libre Office hinter einem in Python generierten Webfrontend anzusprechen oder vieles mehr. Der Artikel beschränkt sich auf die grundlegende Kommunikation mit Libre Office, aber der Fantasie für Erweiterungen sind keine Grenzen gesetzt.

Das Spiel beginnt

Die Voraussetzung schafft der Administrator unter Ubuntu mit der Installation des Pakets »libreoffice-script-provider-python« . Dieses Paket ist allerdings im Kern ein Metapaket, das sich hauptsächlich darum kümmert, dass Libre Office und das Python-Paket »python3-uno« installiert sind. Auf anderen Distributionen, bei denen sich kein Paket namens »libreoffice-script-provider-python« findet, sollte es reichen, die Libre-Office-Pakete und das Paket »python-uno« oder – je nach verwendeter Python-Version – »python3-uno« oder »python2-uno« zu installieren. Das hier entwickelte Skript basiert auf Python 3.

Auf Mac OS X enthält das Libre-Office-Paket einen eigenen Python-Interpreter unter »/Applications/LibreOffice.app/Contents/MacOS/python« . Der hat auch das »uno« -Paket zur Verfügung, daher kann der Admin unter Mac OS X sofort mit dem Scripting beginnen.

Der Python-Interpreter informiert einen auf jeden Fall bereitwillig, ob der Zugriff auf die Libre-Office-Schnittstelle möglich ist. Nach dem Start über das Kommando »python3« oder unter Mac OS »/Applications/LibreOffice.app/Contents/MacOS/python« darf der Import »>>> import uno« nicht fehlschlagen.

Liefert diese Python-Zeile einen »Import Error« , fehlt das »uno« -Paket oder es liegt nicht im erwarteten Pfad für Python-Bibliotheken. Kann Python das Paket jedoch importieren, sollte dem Zugriff auf Libre Office nichts im Wege stehen.

Libre Office beleben

Bevor sich Libre Office fernsteuern lässt, muss der Anwender die Applikation erst einmal starten und sie dazu veranlassen, auf Befehle von außen zu lauschen. Dafür benötigt das »soffice« -Binary die etwas längliche Option:

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

Der Wert der Option deutet an, was Libre Office nun tut: Die Anwendung akzeptiert (»accept=…« ) dann eingehende Anfragen auf dem geöffneten Anschluss (»socket« ), der auf Port 8100 (»port=8100« ) lauscht. Jedenfalls dann, wenn die Anfragen vom eigenen Rechner ausgehen (»host=localhost« ).

Die Option »tcpNoDelay=1« ist empfohlen, um Netzwerkpakete auch bei minimalen Datenmengen sofort loszuschicken. Viele der Fernsteuer-Kommandos an Libre Office sind sehr kurz, und der Anwender wünscht eine sofortige Reaktion. Ohne »tcpNoDelay=1« wartet die Netzwerkschicht noch ab, um zu sehen, ob es nicht noch mehr Daten über die Verbindung zu schicken gilt.

Der abschließende Zusatz »urp;« signalisiert Libre Office, dass die Kommunikation das UNO Remote Protocol benutzen soll. Das ist das Standardprotokoll, das Libre Office für die Kommunikation benutzt. UNO steht für “Universal Network Object” und entspricht auch dem Namen des Python-Pakets, das der Administrator initial installieren muss, um Python die Möglichkeit zu geben, mit Libre Office zu kommunizieren.

Neben dem offenen Port ist es sinnvoll, Libre Office mit der Option »–headless« zu veranlassen, auf alle Anzeigen beim Starten der Anwendung zu verzichten und auch keine weiteren Benutzereingaben zu erlauben. Damit lässt sich die Anwendung nur über den offenen Port steuern und reagiert zügiger auf die Skriptbefehle. Wer allerdings Änderungen am Skript vornimmt, sollte sinnvollerweise diese Option auskommentieren.

Dann kann er beobachten, was Libre Office genau macht. Der vollständige Aufruf sieht dann so aus:

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

Auf der Shell gehört die »accept« -Option dabei in einfache Anführungszeichen, damit die Semikola uninterpretiert an Libre Office weitergehen.

Da es umständlich wäre, Libre Office vor einem Testlauf manuell zu starten, spricht nichts dagegen, die Office-Anwendung gleich aus dem Python-Skript heraus zu öffnen. Das erledigen die Zeilen 13 bis 27 in Listing 1. Die Parameter der Socket-Connection sind im Skript in der Variablen »SOCKET« abgelegt, da diese Werte zum einen beim Start als Option an Libre Office übergeben werden, zum anderen aber auch nötig sind, wenn das Skript die Verbindung auf dem Libre-Office-Socket aufbauen soll.

Listing 1

Fernsteuerung von Libre Office (Teil 1)

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 # Start von Libre Office
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("LibreOffice konnte nicht gestartet werden: %s" % e.message)
025
026 if app.pid <= 0:
027     raise Exception('LibreOffice konnte nicht gestartet werden!')
028
029 # Aufbau der Verbindung zu Libre Office
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('Der Libre-Office-Desktop konnte nicht erzeugt werden!')
055
056 # Ab hier wird Libre Office über die Verbindung gesteuert
057
058 # Initaler "Hallo Welt" 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, 'Hallo Welt!', 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 != 'Hallo Welt!\n'):
079     print("FAIL: \"Hallo Welt\"-Test fehlgeschlagen! Das Resultat war: %s" % text)
080     exitCode = 1
081 else:
082     print('OK: "Hallo Welt!" wird exportiert wie erwartet.')
083
084 document.close(False)
085
086 # Vorbereiten des Test- und Ergebnisordners
087 if sys.argv[1] and path.isdir(sys.argv[1]):
088     testdir = path.abspath(sys.argv[1])
089     resultdir = path.join(testdir, 'Ergebnis_Dateien')
090     if path.isdir(resultdir):
091         rmtree(resultdir)
092     mkdir(resultdir, 0o700)
093
094     # Durchlauf der Testdokumente
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: Datei %s wird exportiert wie erwartet." % testfile)
122             else:
123                 print("FAIL: Datei %s wird nicht exportiert wie erwartet!" % testfile)
124                 exitCode = 1
125         else:
126             print("--: Keine Vergleichsdatei für %s." % testfile)
127         document.close(False)
128
129
130 # Schließen von LibreOffice
131 desktop.terminate()
132
133 sleep(2)
134
135 app.wait()
136
137 sys.exit(exitCode)

Letztendlich führt das Python-Skript am Anfang nichts anderes als den oben beschriebenen »soffice« -Aufruf auf der Kommandozeile aus. Dafür verwendet das Skript in Zeile 18 die Klasse »subprocess.Popen« . Und »soffice« selbst spricht es dabei mit dem vollen Pfad an.

Mac-Variante 1

Unter Mac OS X lautet dieser Aufruf »/Applications/LibreOffice.app/Contents/MacOS/soffice« . Python-gerecht ist der Aufruf über Popen noch in einem »try/expect« -Block gekapselt, um eventuelle Fehler aufzufangen, und damit das Skript den Benutzer bei Bedarf darüber informieren kann, dass der Libre-Office-Start misslungen ist.

Zeit für ein Gespräch

Sobald die Libre-Office-Anwendung läuft, kann das Python-Skript die Fernsteuerung übernehmen. Dafür bemüht das Skript in Zeile 30 die Methode »uno.getComponentContext()« der Python-»uno« -Bibliothek und erzeugt ein allgemeines lokales Kontext-Objekt. Dies verwendet das Skript, um dann in Zeile 31 ein »resolver« -Objekt vom Typ »com.sun.star.bridge.UnoUrlResolver« zu erzeugen. Dieses Objekt bietet eine Methode, mit der das Skript an den Libre-Office-Socket andocken kann.

Da unklar ist, ob Libre Office schon gestartet ist, ist der eigentliche Verbindungsversuch in den Zeilen 36 bis 46 in einen »while« – und einen »try/except« -Block gekapselt. Damit wird alle 0,5 Sekunden versucht, über die »resolve()« -Methode des »resolver« -Objekts eine Verbindung zum Libre-Office-Socket herzustellen. In Zeile 40 kommt auch wieder die initial definierte »SOCKET« -Variable zum Einsatz, sie ist noch um das “Universal Network Object” erweitert, das die laufende Libre-Office-Instanz liefern soll. Das Skript benötigt für die Fernsteuerung den Kontext der laufenden Libre-Office-Anwendung und fragt ihn mit »StarOffice.ComponentContext« an.

Schlägt der Verbindungsversuch fehl, pausiert das Skript für 0,5 Sekunden (»sleep(0.5)« ), setzt den Zähler »n« hoch und probiert es dann noch einmal. Maximal wartet das Skript 6 Sekunden (»n=12« ) lang.

Mac-Variante 2

Auf Mac OS X sollte man das Skript vor eventuellen Verbindungsversuchen (also vor dem »while« -Block) gleich 5 Sekunden schlafen schicken. Unter Mac OS – im Gegensatz zu Linux – enden Verbindungsversuche, während Libre Office hochfährt, gelegentlich im Nirwana und das Skript läuft nicht weiter. Dann ist es sinnvoller, auf den »while« -Block zu verzichten und das Skript immer pausieren zu lassen.

Sobald der »resolver.resolve()« -Aufruf glückt, erhält das Skript in Zeile 39 das Kontext-Objekt der laufenden Libre-Office-Instanz. Damit lässt sich in Zeile 48 ein Objekt vom Typ »com.sun.star.frame.Desktop« erzeugen, das dem Skript nun endlich den Zugriff auf die eigentlichen Funktionen von Libre Office ermöglicht.

An die Arbeit

Um sanft zu starten, beginnt das Skript mit einem denkbar einfachen Funktionstest der Writer-Komponente, also der Textverarbeitung des Office-Pakets. Und zwar schreibt das Skript in ein neues Dokument den Text “Hallo Welt!”. Dafür ist zuerst ein neues Dokument zu öffnen. Das geht mit Hilfe des weiter oben erzeugten »desktop« -Objekts und der Methode »loadComponentFromURL()« (Zeile 59). Diese Methode kann nicht nur neue Dokumente erzeugen, sondern auch bestehende Dateien öffnen, was im nächsten Abschnitt geschehen wird.

Das erste Argument der Funktion ist eine URL, die auf das zu öffnende Dokument verweist. Hier gilt es, ein leeres Dokument zu erzeugen. Die spezielle URL dafür lautet »private:factory/swriter« . Das Dokument soll in einem neuen Fenster stehen und erhält als zweites Argument den »frame« -Parameter mit dem Wert »_blank« für ein neues Fenster.

Den dritten Parameter braucht es in der Situation nicht, er ist deshalb auf »0« gesetzt. Er steht für die Art, ein bestehendes Libre-Office-Fenster zu suchen, wenn der zweite Parameter kein neues Fenster über die Angabe von »_blank« erzeugt.

Das vierte Argument übergibt ein leeres Tupel. Hier könnten Tester Parameter angeben, die das Öffnen des Dokuments beeinflussen. Es ließe sich schreibgeschützt öffnen oder mit der Angabe des Passworts einer geschützten Datei.

Hallo Welt!

Ist das Dokument erzeugt und unter der Variablen »document« abgespeichert, lässt sich auf den Textinhalt über dessen Attribut »Text« zugreifen. Libre Office arbeitet Cursor-basiert und fügt neue Texteingaben an der Position des Cursors hinzu. Um im Dokument schreiben zu können, benötigt das Skript Zugriff auf den Cursor und erhält ihn über das »Text« -Attribut mit »document.Text.createTextCursor()« in Zeile 65.

Die nächste Codezeile verändert den aktuellen Stil und setzt dazu das Attribut »ParaStyleName« des Cursors auf »Heading 1« , was der manuellen Auswahl der Formatierung “Überschrift 1” entspricht.

In der nächsten Zeile fügt das Skript über »document.Text.insertString()« den Text “Hallo Welt!” an der Position des Cursors ein (Abbildung 1). Dazu ist als erstes Argument der Cursor selbst anzugeben, dann der hinzuzufügende Text. Das dritte Argument zeigt an, ob der neue Text den alten überschreiben soll oder ob er eingefügt wird. Die Null in Zeile 67 steht für ein Hinzufügen des Textes.

Abbildung 1: Ohne die »--headless«-Option erscheint im Writer der per Skript befohlene Satz "Hallo Welt!".

Abbildung 1: Ohne die »–headless«-Option erscheint im Writer der per Skript befohlene Satz “Hallo Welt!”.

Das soll an dieser Stelle auch als erster kurzer Test reichen. Was aber noch fehlt, ist eine Überprüfung des Ergebnisses. In diesem ersten Test setzt das Skript auf einen trivialen Ansatz: Nur der Textinhalt des erzeugten Dokuments durchläuft den automatischen Test.

Einmal Text bitte

Für den Textexport erzeugt das Skript in Zeile 69 zuerst einmal eine temporäre Datei über »NamedTemporaryFile()« im Mode »w+« , der es erlaubt, die Datei gleichzeitig zu schreiben und zu lesen. Außerdem legt das Skript noch das Encoding der Datei auf »utf-8-sig« fest. Python weiß damit, dass die Datei UTF-8-Bytes und eine führende Byte-Reihenfolge-Markierung (Byte Order Mark, BOM) enthält. Das ist jenes Format, in dem Libre Office Textdateien exportiert. Weitere Exportfunktionen erläutert der Kasten: “Andere Exportfilter für Calc und Impress”.

Andere Exportfilter mit Calc und Impress

Calc und Impress, das Tabellen- und das Präsentationsprogrmm aus der Libre-Office-Suite, erlauben keinen reinen Text-Export in eine ».txt« -Datei. Dafür stehen andere Text-artige Exportformate bereit. Zum Beispiel CSV für Calc-Tabellen oder HTML für die Impress-Präsentationen. Diese kann das Skript für den automatisierten Abgleich verwenden, es ermöglicht damit den gleichen Ansatz wie beim Test der Writer-Dokumente.

Um ein Calc-Dokument in CSV zu exportieren, sind zwei »PropertyValues« anzugeben:

csvFilterName = PropertyValue()
csvFilterName.Name = 'FilterName'
csvFilterName.Value = 'Text - txt - csv  (StarCalc)'csvFilterOptions = PropertyValue()
csvFilterOptions.Name = 'FilterOptions'
csvFilterOptions.Value = '44,34,0'

Zuerst ist wieder die Angabe für »FilterName« gefragt, in diesem Beispiel »Text – txt – csv (StarCalc)« . Der zweite »PropertyValue« ist notwendig, da der CSV-Export ein paar Parameter benötigt, und zwar die Angabe des Feldtrenners, der Feldbegrenzung und den Character-Set der zu exportierenden Datei. Der zweite »PropertyValue« trägt den Namen »FilterOptions« und den Wert »44,34,0« . Letzterer legt den Feldtrenner auf das Komma (Ascii-Code 44) und die Feldbegrenzung auf doppelte Anführungszeichen (Ascii-Code 34) fest. Die abschließende Null wählt den Standard-Zeichensatz des Systems, was im Normalfall UTF-8 sein sollte.

Für den eigentlichen Export sind dann auch die beiden Property-Values anzugeben:

document.storeToURL(fullPath, (csvFilterName,csvFilterOptions,))

Um Impress-Dokumente in HTML zu exportieren, bedarf es allerdings, wie beim Writer, nur eines »PropertyValue« , und der muss den Namen »FilterName« und den Wert »impress_html_Export« tragen.

Auch für den PDF-Export benötigt das Skript einen etwas anderen Exporter-Namen, wenn es sich um ein Calc- oder Impress-Dokument handelt. So muss der »PropertyValue« im Fall von Calc den passenden Wert »calc_pdf_Export« und im Fall von Impress den Wert »impress_pdf_Export« tragen.

Das Skript instruiert Libre Office dann in den folgenden fünf Zeilen, das aktuelle Dokument als Textdatei in die temporäre Datei zu exportieren. Dafür benennt es zuerst den Exportfilter durch Erzeugen eines »PropertyValue« -Objekts mit Namen »FilterName« und Wert »Text« (Zeilen 71 bis 73).

Zeile 75 weist nachfolgend Libre Office an, mit »document.storeToURL()« das aktuelle Dokument unter der angegebenen URL abzuspeichern. Die URL setzt sich dabei aus dem Präfix »file://« und dem vollständigen Pfad zur temporären Datei (»txtFile.name« ) zusammen. Als zusätzliches Argument erhält diese Funktion eine Liste an »PropertyValue« -Objekten. Hier ist das nur der Verweis auf den Exportfilter, durch den Libre Office weiß, dass es den Inhalt des Dokuments als Textdatei exportieren soll.

Erste Prüfung

Nachdem Libre Office die Datei gefüllt hat, lässt sich überprüfen, ob der Inhalt den Erwartungen entspricht. Dafür liest das Skript den Inhalt der temporären Textdatei kurzerhand mit »txtFile.read()« in Zeile 77 wieder ein. Zeile 78 vergleicht den Inhalt der Datei dann mit dem String »Hallo Welt!\n« . Nur wenn der Text übereinstimmt, quittiert das Skript dies mit einer »OK: …« -Meldung (Zeile 82). Ansonsten erzeugt es eine »FAIL: …« -Meldung in Zeile 79 und setzt die »exitCode« -Variable auf den Wert »1« .

Diesen ersten Testlauf beendet das Skript, indem es das Dokument in Zeile 84 mit der Methode »close()« schließt. Der Parameter »False« hindert Libre Office daran, den Nutzer zu fragen, ob es das Dokument vor dem Schließen speichern soll.

Augenschein

Bei diesem trivialen Test darf der Administrator zu Recht hoffen, dass er nicht fehlschlägt. Das Skript nutzt nun im letzten Abschnitt die bisher etablieren Techniken, um ein Libre-Office-Update innerhalb des gegebenen Szenarios – eine Vielzahl an Abteilungen mit unterschiedlichen Office-Formulartypen – möglichst risikoarm zu gestalten.

Zum einen setzt das Skript auch hier wieder auf den einfachen Textvergleich. Was sich bei Weitem nicht so einfach automatisieren lässt, ist die Prüfung auf ein fehlerhaftes Layout. Das korrekte Layout ließe sich vermutlich nur über Screenshots einfangen. Es ist aber vom Aufwand her nicht sinnvoll, einen Screenshot des Dokuments gegen ein fixes Zielbild abzugleichen. Dieser Test wäre auch extrem fragil. Denn natürlich muss die Libre-Office-Darstellung von Update zu Update nicht pixelgenau konstant bleiben.

Das menschliche Auge ist deutlich besser darin, Layout-Probleme zu erkennen. Um also Aufwand und Nutzen vernünftig auszugleichen, nutzt das Skript einen geteilten Ansatz: Nur der Textinhalt der Dateien durchläuft den automatisierten Test, für die Überprüfung des Layouts erzeugt es ein PDF, das dann ein Mensch in Augenschein nimmt.

Wichtig für diesen zweiten Test ist, dass der Admin zuvor die zentralen Formulare aus den verschiedenen Abteilungen in einem Ordner auf der Festplatte zusammengetragen hat. Der Pfad zu dem entsprechenden Verzeichnis ist dem Skript beim Aufruf als einzelnes Argument auf der Kommandozeile zu übergeben. Nur wenn dieses Argument vorhanden ist und auf ein existierendes Verzeichnis verweist, führt das Skript den zweiten Test durch (Zeile 87).

In den Zeilen 88 bis 92 von Listing 1 legt sich das Skript in der Variablen »testdir« den absoluten Pfad zum Testverzeichnis an (Zeile 88). Außerdem erzeugt es einen weiteren Ordner in diesem Verzeichnis. Dieser trägt den Namen »Ergebnis_Dateien« , und das Skript legt ihn in der Variablen »resultdir« ab (Zeile 89). Sollte schon vorher ein solches Verzeichnis existieren, löscht Zeile 91 es mittels »rmtree()« . In jedem Fall legt das Skript das Verzeichnis neu an (Zeile 92).

Da Libre Office in diesem Beispiel auch PDF-Dateien erzeugen soll, bereiten die Zeilen 95 bis 97 ein entsprechendes »PropertyValue« -Objekt vor, das jetzt nicht den Wert »Text« , sondern »writer_pdf_export« trägt.

Im Folgenden geht das Skript davon aus, dass der Administrator alle zu testenden Formulare im Testverzeichnis mit dem Präfix »Test_’« versehen hat. Mit der Funktion »glob()« in Zeile 99 erhält das Skript nun alle Dateien, die vom Namensschema her passen. Die bereits beschriebene Funktion »desktop.loadComponentFromURL()« in Zeile 102 lädt jetzt jede der Dateien. Diesmal ist das erste Argument jedoch ein Verweis auf eine bereits bestehende Office-Datei. Die URL erstellt es, indem es dem absoluten Pfad der Testdatei das Präfix »’file://’« voranstellt.

Libre Office öffnet erneut ein neues Fenster und verwaltet dort die Datei. Den Zugriff auf dieses Fenster verwaltet wieder die Variable »document« . Ob ein Writer-, Calc- oder Impress-Fenster zu öffnen ist, erkennt Libre Office automatisch am Inhalt der zu ladenden Datei. Kommentiert der Administrator die »–headless« -Option am Anfang der Sktiptdatei aus, zeigt Libre Office die Datei (Abbildung 2). Diesmal schreibt das Skript allerdings nichts in das Dokument, sondern exportiert es nur in zwei verschiedene Formate. Dafür generiert es sich in Zeile 109 mit »path.basename(testfile)[5:-4]« den verkürzten Dateinamen des Testformulars. Also zum Beispiel »Abteilung1_Bestellformular« aus »Test_Abteilung1_Bestellformular.doc« .

Abbildung 2: Das Skript instruiert Libre Office, beliebig komplexe Dateien zu öffnen.

Abbildung 2: Das Skript instruiert Libre Office, beliebig komplexe Dateien zu öffnen.

Nun speichert das Skript das Ursprungsformular zum einen in dem Ordner »Ergebnis_Dateien« als »PDF_Abteilung1_Bestellformular.pdf« (Zeilen 111 bis 112) sowie unter »Text_Abteilung1_Bestellformular.txt« (Zeilen 114 bis 116) in den entsprechenden Formaten.

Sofern der Admin im eigentlichen Testverzeichnis neben den zu prüfenden Formularen eine namentlich abgestimmte Textdatei platziert hat – so wie oben zum Beispiel »Text_Abteilung1_Bestellformular.txt« –, gleicht das Skript diese Datei mit dem passenden Export im Ergebnis-Ordner ab. Das geschieht in Zeile 120 mit Hilfe der Funktion »filecmp.cpm« . Passt der Inhalt, gibt Zeile 121 eine »OK: …« -Meldung aus (Abbildung 3). Gibt es Unterschiede, führt das in Zeile 123 zu einer »FAIL: …« -Meldung. In diesem Fall wird auch der Exitcode für das Skript auf »1« – also kein erfolgreicher Durchlauf – festgelegt.

Abbildung 3: Die Ausgabe des Skripts bei einem kompletten Testdurchlauf.

Abbildung 3: Die Ausgabe des Skripts bei einem kompletten Testdurchlauf.

Hat der Administrator keine Textdatei in den Testordner gelegt, vermerkt das Skript dies in Zeile 126 mit der Meldung »–: Keine Vergleichsdatei für Test_Abteilung1_Bestellformular.doc« . Tipp: Die Textdateien lassen sich am einfachsten erzeugen, indem das Skript vor dem Update einmal mit der alten Libre-Office-Version läuft und die dabei erzeugten Textdateien aus dem Ergebnisordner in den Testordner kopiert. Das PDF ist für den Administrator oder die entsprechenden Abteilungen gedacht. Layout-Probleme der neuen Libre-Office-Version sollten sich über einen Ausdruck einfach identifizieren lassen.

Der zweite Testabschnitt beendet jeden Formular-Test damit, das zugehörige geöffnete Fenster in Zeile 127 mit »document.close()« wieder zu schließen.

Abschlussprüfung

Abschließend endet das gesamte Skript, indem es den Libre-Office-Desktop in Zeile 131 mit »desktop.terminate()« schließt. Das Skript wartet noch ein wenig (Zeile 135), bis Libre Office sich ordnungsgemäß beendet hat. Dann schließt es den Testlauf in Zeile 137 ab und gibt – sofern alle Tests erfolgreich durchgelaufen sind – einen Exitcode von »0« (für erfolgreich) zurück. Ansonsten signalisiert der Exitcode »1« , dass ein oder mehrere Test fehlgeschlagen sind.

Robustes Werkzeug

Listing 1 demonstriert die Grundzüge der Fernsteuerung von Libre Office. Nicht für jeden Administrator mögen das beschriebene Szenario und die skizzierte Lösung passend sein. Aber grundsätzlich steht jedem mit ein bisschen Python-Scripting-Know-how über die Fernsteuerung von Libre Office ein enormes Kraftpaket zur Verfügung. In jedem Bereich, der sehr viel mit den typischen Office-Formaten zu tun hat, dürften sich schnell Testaufgaben finden, bei denen sich Skript-gesteuerte Lösungen anbieten.

Auf Github etwa tummelt sich eine ganze Reihe von Konvertierungstools für Dokumente verschiedenster Art, die auf der Fernsteuerung von Libre oder Open Office beruhen. Oft sind solche Konverter auch hinter einem Webfrontend versteckt. Github hostet aber auch Werkzeuge für die Extraktion von Elementen aus Office-Dokumenten, für die Generierung von Reports, die Erstellung von Dokumenten-Diffs und noch vieles mehr. Nützlich ist die Technik also allemal.

Einen Haken gibt es allerdings: die Dokumentation. Ja, es gibt die eigene Seite des Libre-Office-API [1] und auch eine detaillierte Dokumentation der Schnittstelle [2]. Aber erstere verlinkt nur ein paar sehr knappe Python-Beispiele und letztere bezieht sich auf die C++-Seite der Schnittstelle. Wie die einzelnen Aufrufe unter Python aussehen, lässt sich nicht immer so einfach ableiten.

Dass es nicht nur Libre Office, sondern auch Open Office gibt, macht die Angelegenheit auch nicht unbedingt einfacher. Das API scheint zwar derzeit das Gleiche zu sein, aber teilweise finden sich im Open-Office-Wiki [3] aussagekräftigere Beispiele. Ob sich die APIs längerfristig stärker auseinanderentwickeln, lässt sich schwer beurteilen. Der Entwickler eines entsprechenden Python-Skripts kommt letztlich nicht um Google, Stack Overflow und den öffentlichen Code auf Github herum. Die auffindbaren Codefragmente helfen dem Entwickler viel eher als die abstrakte API-Dokumentation.

Sind die funktionierenden Aufrufe erst einmal gesammelt, funktioniert das Ganze erstaunlich robust und liefert Administratoren in einer Office-lastigen Umgebung einen vielseitigen Werkzeugkasten für Tests.

Infos

  1. Libre-Office-API:http://api.libreoffice.org
  2. Dokumentation des Libre-Office-API: http://api.libreoffice.org/docs/idl/ref/index.html
  3. Open-Office-Wiki:https://wiki.openoffice.org

Der Autor

Gunnar Wrobel arbeitet als Freelancer im Bereich Web-Development und schwört dabei – auch entgegen allen bekannten Widerständen – auf Emacs.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 7 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben