1 #!/usr/bin/env python # -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
3 # Version: MPL 1.1 / GPLv3+ / LGPLv3+
5 # The contents of this file are subject to the Mozilla Public License Version
6 # 1.1 (the "License"); you may not use this file except in compliance with
7 # the License or as specified alternatively below. You may obtain a copy of
8 # the License at http://www.mozilla.org/MPL/
10 # Software distributed under the License is distributed on an "AS IS" basis,
11 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 # for the specific language governing rights and limitations under the
15 # Major Contributor(s):
16 # Copyright (C) 2012 Red Hat, Inc., Michael Stahl <mstahl@redhat.com>
19 # All Rights Reserved.
21 # For minor contributions see the git repository.
23 # Alternatively, the contents of this file may be used under the terms of
24 # either the GNU General Public License Version 3 or later (the "GPLv3+"), or
25 # the GNU Lesser General Public License Version 3 or later (the "LGPLv3+"),
26 # in which case the provisions of the GPLv3+ or the LGPLv3+ are applicable
27 # instead of those above.
29 # Simple script to load a bunch of documents and export them as Flat ODF
31 # Personally I run it like this:
32 # ~/lo/master-suse/instdir/program/python ~/lo/master-suse/bin/benchmark-document-loading --soffice=path:/home/tml/lo/master-suse/instdir/program/soffice --outdir=file://$PWD/out --userdir=file:///tmp/test $PWD/docs
44 from urllib
.parse
import quote
46 from urllib
import quote
54 print("pyuno not found: try to set PYTHONPATH and URE_BOOTSTRAP variables")
55 print("PYTHONPATH=/installation/opt/program")
56 print("URE_BOOTSTRAP=file:///installation/opt/program/fundamentalrc")
60 from com
.sun
.star
.document
import XDocumentEventListener
61 from com
.sun
.star
.io
import XOutputStream
63 print("UNO API class not found: try to set URE_BOOTSTRAP variable")
64 print("URE_BOOTSTRAP=file:///installation/opt/program/fundamentalrc")
67 validCalcFileExtensions
= [ ".xlsx", ".xls", ".ods", ".fods" ]
68 validWriterFileExtensions
= [ ".docx" , ".rtf", ".odt", ".fodt", ".doc" ]
69 validImpressFileExtensions
= [ ".ppt", ".pptx", ".odp", ".fodp" ]
70 validDrawFileExtensions
= [ ".odg", ".fodg" ]
71 validReverseFileExtensions
= [ ".vsd", ".vdx", ".cdr", ".pub", ".wpd" ]
72 validFileExtensions
= {"calc": validCalcFileExtensions
,
73 "writer": validWriterFileExtensions
,
74 "impress": validImpressFileExtensions
,
75 "draw": validDrawFileExtensions
,
76 "reverse": validReverseFileExtensions
}
77 flatODFTypes
= {"calc": (".fods", "OpenDocument Spreadsheet Flat XML"),
78 "writer": (".fodt", "OpenDocument Text Flat XML"),
79 "impress": (".fodp", "OpenDocument Presentation Flat XML"),
80 "draw": (".fodg", "OpenDocument Drawing Flat XML")}
84 def partition(list, pred
):
94 def filelist(directory
, suffix
):
96 raise Exception("filelist: empty directory")
97 if directory
[-1] != "/":
99 files
= [directory
+ f
for f
in os
.listdir(directory
)]
101 return [f
for f
in files
102 if os
.path
.isfile(f
) and os
.path
.splitext(f
)[1] == suffix
]
104 def getFiles(dirs
, suffix
):
108 files
+= filelist(d
, suffix
)
111 ### UNO utilities ###
113 class OutputStream( unohelper
.Base
, XOutputStream
):
114 def __init__( self
):
117 def closeOutput(self
):
120 def writeBytes( self
, seq
):
121 sys
.stdout
.write( seq
.value
)
126 class OfficeConnection
:
127 def __init__(self
, args
):
134 (method
, sep
, rest
) = self
.args
.soffice
.partition(":")
136 raise Exception("soffice parameter does not specify method")
138 socket
= "pipe,name=pytest" + str(uuid
.uuid1())
139 userdir
= self
.args
.userdir
141 raise Exception("'path' method requires --userdir")
142 if not userdir
.startswith("file://"):
143 raise Exception("--userdir must be file URL")
144 self
.soffice
= self
.bootstrap(rest
, userdir
, socket
)
145 elif method
== "connect":
148 raise Exception("unsupported connection method: " + method
)
149 self
.xContext
= self
.connect(socket
)
151 def bootstrap(self
, soffice
, userdir
, socket
):
152 argv
= [ soffice
, "--accept=" + socket
+ ";urp",
153 "-env:UserInstallation=" + userdir
,
155 "--norestore", "--nologo", "--headless" ]
156 if self
.args
.valgrind
:
157 argv
.append("--valgrind")
158 os
.putenv("SAL_LOG", "-INFO-WARN")
159 os
.putenv("LIBO_ONEWAY_STABLE_ODF_EXPORT", "YES")
160 self
.pro
= subprocess
.Popen(argv
)
161 # print(self.pro.pid)
163 def connect(self
, socket
):
164 xLocalContext
= uno
.getComponentContext()
165 xUnoResolver
= xLocalContext
.ServiceManager
.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", xLocalContext
)
166 url
= "uno:" + socket
+ ";urp;StarOffice.ComponentContext"
167 # print("OfficeConnection: connecting to: " + url)
170 xContext
= xUnoResolver
.resolve(url
)
172 # except com.sun.star.connection.NoConnectException
173 except pyuno
.getClass("com.sun.star.connection.NoConnectException"):
174 # print("NoConnectException: sleeping...")
181 # print("tearDown: calling terminate()...")
182 xMgr
= self
.xContext
.ServiceManager
183 xDesktop
= xMgr
.createInstanceWithContext("com.sun.star.frame.Desktop", self
.xContext
)
186 # except com.sun.star.lang.DisposedException:
187 except pyuno
.getClass("com.sun.star.beans.UnknownPropertyException"):
188 # print("caught UnknownPropertyException while TearDown")
189 pass # ignore, also means disposed
190 except pyuno
.getClass("com.sun.star.lang.DisposedException"):
191 # print("caught DisposedException while TearDown")
194 self
.soffice
.terminate()
195 ret
= self
.soffice
.wait()
200 raise Exception("Exit status indicates failure: " + str(ret
))
203 command
= "kill " + str(self
.pro
.pid
)
204 with
open("killFile.log", "a") as killFile
:
205 killFile
.write(command
+ "\n")
210 class PersistentConnection
:
211 def __init__(self
, args
):
213 self
.connection
= None
214 def getContext(self
):
215 return self
.connection
.xContext
217 assert(not self
.connection
)
218 conn
= OfficeConnection(self
.args
)
220 self
.connection
= conn
222 assert(self
.connection
)
224 assert(self
.connection
)
228 self
.connection
.tearDown()
230 self
.connection
= None
233 self
.connection
.kill()
235 def simpleInvoke(connection
, test
):
238 test
.run(connection
.getContext(), connection
)
240 connection
.postTest()
242 def runConnectionTests(connection
, invoker
, tests
):
246 invoker(connection
, test
)
249 #connection.tearDown()
251 class EventListener(XDocumentEventListener
,unohelper
.Base
):
253 self
.layoutFinished
= False
254 def documentEventOccured(self
, event
):
255 # print(str(event.EventName))
256 if event
.EventName
== "OnLayoutFinished":
257 self
.layoutFinished
= True
258 def disposing(event
):
261 def mkPropertyValue(name
, value
):
262 return uno
.createUnoStruct("com.sun.star.beans.PropertyValue",
267 def logTimeSpent(url
, startTime
):
268 print(os
.path
.basename(urllib
.parse
.urlparse(url
).path
) + "\t" + str(time
.time()-startTime
))
270 def loadFromURL(xContext
, url
, t
, component
):
271 xDesktop
= xContext
.ServiceManager
.createInstanceWithContext("com.sun.star.frame.Desktop", xContext
)
272 props
= [("Hidden", True), ("ReadOnly", True)] # FilterName?
273 loadProps
= tuple([mkPropertyValue(name
, value
) for (name
, value
) in props
])
275 if component
== "writer":
276 xListener
= EventListener()
277 xGEB
= xContext
.getValueByName(
278 "/singletons/com.sun.star.frame.theGlobalEventBroadcaster")
279 xGEB
.addDocumentEventListener(xListener
)
282 startTime
= time
.time()
283 xDoc
= xDesktop
.loadComponentFromURL(url
, "_blank", 0, loadProps
)
284 if component
== "calc":
288 except AttributeError:
291 logTimeSpent(url
, startTime
)
293 elif component
== "writer":
297 if xListener
.layoutFinished
:
298 logTimeSpent(url
, startTime
)
300 # print("delaying...")
305 logTimeSpent(url
, startTime
)
307 with
open("file.log", "a") as fh
:
308 fh
.write("layout did not finish\n")
310 except pyuno
.getClass("com.sun.star.beans.UnknownPropertyException"):
312 raise # means crashed, handle it later
313 except pyuno
.getClass("com.sun.star.lang.DisposedException"):
315 raise # means crashed, handle it later
316 except pyuno
.getClass("com.sun.star.lang.IllegalArgumentException"):
317 pass # means could not open the file, ignore it
325 xGEB
.removeDocumentEventListener(xListener
)
327 def exportToODF(xContext
, xDoc
, baseName
, t
, component
):
328 exportFileName
= outdir
+ "/" + os
.path
.splitext(baseName
)[0] + flatODFTypes
[component
][0]
329 print("exportToODF " + baseName
+ " => " + exportFileName
)
330 props
= [("FilterName", flatODFTypes
[component
][1]),
332 storeProps
= tuple([mkPropertyValue(name
, value
) for (name
, value
) in props
])
333 xDoc
.storeToURL(exportFileName
, tuple(storeProps
))
335 def handleCrash(file, disposed
):
336 # print("File: " + file + " crashed")
337 with
open("crashlog.txt", "a") as crashLog
:
338 crashLog
.write('Crash:' + file + ' ')
340 crashLog
.write('through disposed\n')
341 # crashed_files.append(file)
342 # add here the remaining handling code for crashed files
344 def alarm_handler(args
):
347 class HandleFileTest
:
348 def __init__(self
, file, state
, component
):
351 self
.component
= component
352 def run(self
, xContext
, connection
):
353 # print("Loading document: " + self.file)
357 url
= "file://" + quote(self
.file)
358 with
open("file.log", "a") as fh
:
362 t
= threading
.Timer(60, alarm_handler
, args
)
364 xDoc
= loadFromURL(xContext
, url
, t
, self
.component
)
365 self
.state
.goodFiles
.append(self
.file)
366 exportToODF(xContext
, xDoc
, os
.path
.basename(urllib
.parse
.urlparse(url
).path
), t
, self
.component
)
367 except pyuno
.getClass("com.sun.star.beans.UnknownPropertyException"):
368 # print("caught UnknownPropertyException " + self.file)
371 self
.state
.timeoutFiles
.append(self
.file)
374 handleCrash(self
.file, 0)
375 self
.state
.badPropertyFiles
.append(self
.file)
376 connection
.tearDown()
378 except pyuno
.getClass("com.sun.star.lang.DisposedException"):
379 # print("caught DisposedException " + self.file)
382 self
.state
.timeoutFiles
.append(self
.file)
385 handleCrash(self
.file, 1)
386 self
.state
.badDisposedFiles
.append(self
.file)
387 connection
.tearDown()
394 t
= threading
.Timer(10, alarm_handler
, args
)
398 except pyuno
.getClass("com.sun.star.beans.UnknownPropertyException"):
399 print("caught UnknownPropertyException while closing")
400 self
.state
.badPropertyFiles
.append(self
.file)
401 connection
.tearDown()
403 except pyuno
.getClass("com.sun.star.lang.DisposedException"):
404 print("caught DisposedException while closing")
408 self
.state
.badDisposedFiles
.append(self
.file)
409 connection
.tearDown()
411 # print("...done with: " + self.file)
416 self
.badDisposedFiles
= []
417 self
.badPropertyFiles
= []
418 self
.timeoutFiles
= []
421 def write_state_report(files_list
, start_time
, report_filename
, description
):
422 with
open(report_filename
, "w") as fh
:
423 fh
.write("%s:\n" % description
)
424 fh
.write("Starttime: %s\n" % start_time
.isoformat())
429 def writeReport(state
, startTime
):
430 write_state_report(state
.goodFiles
, startTime
, "goodFiles.log",
431 "Files which loaded perfectly")
432 write_state_report(state
.badDisposedFiles
, startTime
, "badDisposedFiles.log",
433 "Files which crashed with DisposedException")
434 write_state_report(state
.badPropertyFiles
, startTime
, "badPropertyFiles.log",
435 "Files which crashed with UnknownPropertyException")
436 write_state_report(state
.timeoutFiles
, startTime
, "timeoutFiles.log",
437 "Files which timed out")
439 def runHandleFileTests(opts
):
440 startTime
= datetime
.datetime
.now()
441 connection
= PersistentConnection(opts
)
443 outdir
= os
.path
.join(opts
.outdir
, startTime
.strftime('%Y%m%d.%H%M%S'))
447 # print("before map")
448 for component
, validExtension
in validFileExtensions
.items():
450 for suffix
in validExtension
:
451 files
.extend(getFiles(opts
.dirs
, suffix
))
453 tests
.extend( (HandleFileTest(file, state
, component
) for file in files
) )
454 runConnectionTests(connection
, simpleInvoke
, tests
)
457 writeReport(state
, startTime
)
460 epilog
= "'location' is a pathname, not a URL. 'outdir' and 'userdir' are URLs.\n" \
461 "The 'directory' parameters should be full absolute pathnames, not URLs."
463 parser
= argparse
.ArgumentParser(formatter_class
=argparse
.RawTextHelpFormatter
,
465 parser
.add_argument('--soffice', metavar
='method:location', required
=True,
466 help="specify soffice instance to connect to\n"
467 "supported methods: 'path', 'connect'")
468 parser
.add_argument('--outdir', metavar
='URL', required
=True,
469 help="specify the output directory for flat ODF exports")
470 parser
.add_argument('--userdir', metavar
='URL',
471 help="specify user installation directory for 'path' method")
472 parser
.add_argument('--valgrind', action
='store_true',
473 help="pass --valgrind to soffice for 'path' method")
474 parser
.add_argument('dirs', metavar
='directory', nargs
='+')
476 args
= parser
.parse_args(argv
[1:])
481 if __name__
== "__main__":
482 opts
= parseArgs(sys
.argv
)
483 runHandleFileTests(opts
)
485 # vim:set shiftwidth=4 softtabstop=4 expandtab: