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
.beans
import PropertyValue
61 from com
.sun
.star
.document
import XDocumentEventListener
62 from com
.sun
.star
.io
import IOException
, XOutputStream
64 print("UNO API class not found: try to set URE_BOOTSTRAP variable")
65 print("URE_BOOTSTRAP=file:///installation/opt/program/fundamentalrc")
68 validCalcFileExtensions
= [ ".xlsx", ".xls", ".ods", ".fods" ]
69 validWriterFileExtensions
= [ ".docx" , ".rtf", ".odt", ".fodt", ".doc" ]
70 validImpressFileExtensions
= [ ".ppt", ".pptx", ".odp", ".fodp" ]
71 validDrawFileExtensions
= [ ".odg", ".fodg" ]
72 validRevereseFileExtensions
= [ ".vsd", ".vdx", ".cdr", ".pub", ".wpd" ]
73 validFileExtensions
= {"calc": validCalcFileExtensions
,
74 "writer": validWriterFileExtensions
,
75 "impress": validImpressFileExtensions
,
76 "draw": validDrawFileExtensions
,
77 "reverse": validRevereseFileExtensions
}
78 flatODFTypes
= {"calc": (".fods", "OpenDocument Spreadsheet Flat XML"),
79 "writer": (".fodt", "OpenDocument Text Flat XML"),
80 "impress": (".fodp", "OpenDocument Presentation Flat XML"),
81 "draw": (".fodg", "OpenDocument Drawing Flat XML")}
85 def partition(list, pred
):
95 def filelist(directory
, suffix
):
97 raise Exception("filelist: empty directory")
98 if directory
[-1] != "/":
100 files
= [directory
+ f
for f
in os
.listdir(directory
)]
102 return [f
for f
in files
103 if os
.path
.isfile(f
) and os
.path
.splitext(f
)[1] == suffix
]
105 def getFiles(dirs
, suffix
):
109 files
+= filelist(d
, suffix
)
112 ### UNO utilities ###
114 class OutputStream( unohelper
.Base
, XOutputStream
):
115 def __init__( self
):
118 def closeOutput(self
):
121 def writeBytes( self
, seq
):
122 sys
.stdout
.write( seq
.value
)
127 class OfficeConnection
:
128 def __init__(self
, args
):
135 (method
, sep
, rest
) = self
.args
.soffice
.partition(":")
137 raise Exception("soffice parameter does not specify method")
139 socket
= "pipe,name=pytest" + str(uuid
.uuid1())
140 userdir
= self
.args
.userdir
142 raise Exception("'path' method requires --userdir")
143 if not userdir
.startswith("file://"):
144 raise Exception("--userdir must be file URL")
145 self
.soffice
= self
.bootstrap(rest
, userdir
, socket
)
146 elif method
== "connect":
149 raise Exception("unsupported connection method: " + method
)
150 self
.xContext
= self
.connect(socket
)
152 def bootstrap(self
, soffice
, userdir
, socket
):
153 argv
= [ soffice
, "--accept=" + socket
+ ";urp",
154 "-env:UserInstallation=" + userdir
,
156 "--norestore", "--nologo", "--headless" ]
157 if self
.args
.valgrind
:
158 argv
.append("--valgrind")
159 os
.putenv("SAL_LOG", "-INFO-WARN")
160 os
.putenv("LIBO_ONEWAY_STABLE_ODF_EXPORT", "YES")
161 self
.pro
= subprocess
.Popen(argv
)
162 # print(self.pro.pid)
164 def connect(self
, socket
):
165 xLocalContext
= uno
.getComponentContext()
166 xUnoResolver
= xLocalContext
.ServiceManager
.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", xLocalContext
)
167 url
= "uno:" + socket
+ ";urp;StarOffice.ComponentContext"
168 # print("OfficeConnection: connecting to: " + url)
171 xContext
= xUnoResolver
.resolve(url
)
173 # except com.sun.star.connection.NoConnectException
174 except pyuno
.getClass("com.sun.star.connection.NoConnectException"):
175 # print("NoConnectException: sleeping...")
182 # print("tearDown: calling terminate()...")
183 xMgr
= self
.xContext
.ServiceManager
184 xDesktop
= xMgr
.createInstanceWithContext("com.sun.star.frame.Desktop", self
.xContext
)
187 # except com.sun.star.lang.DisposedException:
188 except pyuno
.getClass("com.sun.star.beans.UnknownPropertyException"):
189 # print("caught UnknownPropertyException while TearDown")
190 pass # ignore, also means disposed
191 except pyuno
.getClass("com.sun.star.lang.DisposedException"):
192 # print("caught DisposedException while TearDown")
195 self
.soffice
.terminate()
196 ret
= self
.soffice
.wait()
201 raise Exception("Exit status indicates failure: " + str(ret
))
204 command
= "kill " + str(self
.pro
.pid
)
205 with
open("killFile.log", "a") as killFile
:
206 killFile
.write(command
+ "\n")
211 class PersistentConnection
:
212 def __init__(self
, args
):
214 self
.connection
= None
215 def getContext(self
):
216 return self
.connection
.xContext
218 assert(not self
.connection
)
219 conn
= OfficeConnection(self
.args
)
221 self
.connection
= conn
223 assert(self
.connection
)
225 assert(self
.connection
)
229 self
.connection
.tearDown()
231 self
.connection
= None
234 self
.connection
.kill()
236 def simpleInvoke(connection
, test
):
239 test
.run(connection
.getContext(), connection
)
241 connection
.postTest()
243 def runConnectionTests(connection
, invoker
, tests
):
247 invoker(connection
, test
)
250 #connection.tearDown()
252 class EventListener(XDocumentEventListener
,unohelper
.Base
):
254 self
.layoutFinished
= False
255 def documentEventOccured(self
, event
):
256 # print(str(event.EventName))
257 if event
.EventName
== "OnLayoutFinished":
258 self
.layoutFinished
= True
259 def disposing(event
):
262 def mkPropertyValue(name
, value
):
263 return uno
.createUnoStruct("com.sun.star.beans.PropertyValue",
268 def logTimeSpent(url
, startTime
):
269 print(os
.path
.basename(urllib
.parse
.urlparse(url
).path
) + "\t" + str(time
.time()-startTime
))
271 def loadFromURL(xContext
, url
, t
, component
):
272 xDesktop
= xContext
.ServiceManager
.createInstanceWithContext("com.sun.star.frame.Desktop", xContext
)
273 props
= [("Hidden", True), ("ReadOnly", True)] # FilterName?
274 loadProps
= tuple([mkPropertyValue(name
, value
) for (name
, value
) in props
])
276 if component
== "writer":
277 xListener
= EventListener()
278 xGEB
= xContext
.getValueByName(
279 "/singletons/com.sun.star.frame.theGlobalEventBroadcaster")
280 xGEB
.addDocumentEventListener(xListener
)
283 startTime
= time
.time()
284 xDoc
= xDesktop
.loadComponentFromURL(url
, "_blank", 0, loadProps
)
285 if component
== "calc":
289 except AttributeError:
292 logTimeSpent(url
, startTime
)
294 elif component
== "writer":
298 if xListener
.layoutFinished
:
299 logTimeSpent(url
, startTime
)
301 # print("delaying...")
306 logTimeSpent(url
, startTime
)
308 with
open("file.log", "a") as fh
:
309 fh
.write("layout did not finish\n")
311 except pyuno
.getClass("com.sun.star.beans.UnknownPropertyException"):
313 raise # means crashed, handle it later
314 except pyuno
.getClass("com.sun.star.lang.DisposedException"):
316 raise # means crashed, handle it later
317 except pyuno
.getClass("com.sun.star.lang.IllegalArgumentException"):
318 pass # means could not open the file, ignore it
326 xGEB
.removeDocumentEventListener(xListener
)
328 def exportToODF(xContext
, xDoc
, baseName
, t
, component
):
329 exportFileName
= outdir
+ "/" + os
.path
.splitext(baseName
)[0] + flatODFTypes
[component
][0]
330 print("exportToODF " + baseName
+ " => " + exportFileName
)
331 props
= [("FilterName", flatODFTypes
[component
][1]),
333 storeProps
= tuple([mkPropertyValue(name
, value
) for (name
, value
) in props
])
334 xDoc
.storeToURL(exportFileName
, tuple(storeProps
))
336 def handleCrash(file, disposed
):
337 # print("File: " + file + " crashed")
338 with
open("crashlog.txt", "a") as crashLog
:
339 crashLog
.write('Crash:' + file + ' ')
341 crashLog
.write('through disposed\n')
342 # crashed_files.append(file)
343 # add here the remaining handling code for crashed files
345 def alarm_handler(args
):
348 class HandleFileTest
:
349 def __init__(self
, file, state
, component
):
352 self
.component
= component
353 def run(self
, xContext
, connection
):
354 # print("Loading document: " + self.file)
358 url
= "file://" + quote(self
.file)
359 with
open("file.log", "a") as fh
:
363 t
= threading
.Timer(60, alarm_handler
, args
)
365 xDoc
= loadFromURL(xContext
, url
, t
, self
.component
)
366 self
.state
.goodFiles
.append(self
.file)
367 exportToODF(xContext
, xDoc
, os
.path
.basename(urllib
.parse
.urlparse(url
).path
), t
, self
.component
)
368 except pyuno
.getClass("com.sun.star.beans.UnknownPropertyException"):
369 # print("caught UnknownPropertyException " + self.file)
372 self
.state
.timeoutFiles
.append(self
.file)
375 handleCrash(self
.file, 0)
376 self
.state
.badPropertyFiles
.append(self
.file)
377 connection
.tearDown()
379 except pyuno
.getClass("com.sun.star.lang.DisposedException"):
380 # print("caught DisposedException " + self.file)
383 self
.state
.timeoutFiles
.append(self
.file)
386 handleCrash(self
.file, 1)
387 self
.state
.badDisposedFiles
.append(self
.file)
388 connection
.tearDown()
395 t
= threading
.Timer(10, alarm_handler
, args
)
399 except pyuno
.getClass("com.sun.star.beans.UnknownPropertyException"):
400 print("caught UnknownPropertyException while closing")
401 self
.state
.badPropertyFiles
.append(self
.file)
402 connection
.tearDown()
404 except pyuno
.getClass("com.sun.star.lang.DisposedException"):
405 print("caught DisposedException while closing")
409 self
.state
.badDisposedFiles
.append(self
.file)
410 connection
.tearDown()
412 # print("...done with: " + self.file)
417 self
.badDisposedFiles
= []
418 self
.badPropertyFiles
= []
419 self
.timeoutFiles
= []
422 def write_state_report(files_list
, start_time
, report_filename
, description
):
423 with
open(report_filename
, "w") as fh
:
424 fh
.write("%s:\n" % description
)
425 fh
.write("Starttime: %s\n" % start_time
.isoformat())
430 def writeReport(state
, startTime
):
431 write_state_report(state
.goodFiles
, startTime
, "goodFiles.log",
432 "Files which loaded perfectly")
433 write_state_report(state
.badDisposedFiles
, startTime
, "badDisposedFiles.log",
434 "Files which crashed with DisposedException")
435 write_state_report(state
.badPropertyFiles
, startTime
, "badPropertyFiles.log",
436 "Files which crashed with UnknownPropertyException")
437 write_state_report(state
.timeoutFiles
, startTime
, "timeoutFiles.log",
438 "Files which timed out")
440 def runHandleFileTests(opts
):
441 startTime
= datetime
.datetime
.now()
442 connection
= PersistentConnection(opts
)
444 outdir
= os
.path
.join(opts
.outdir
, startTime
.strftime('%Y%m%d.%H%M%S'))
448 # print("before map")
449 for component
, validExtension
in validFileExtensions
.items():
451 for suffix
in validExtension
:
452 files
.extend(getFiles(opts
.dirs
, suffix
))
454 tests
.extend( (HandleFileTest(file, state
, component
) for file in files
) )
455 runConnectionTests(connection
, simpleInvoke
, tests
)
458 writeReport(state
, startTime
)
461 epilog
= "'location' is a pathname, not a URL. 'outdir' and 'userdir' are URLs.\n" \
462 "The 'directory' parameters should be full absolute pathnames, not URLs."
464 parser
= argparse
.ArgumentParser(formatter_class
=argparse
.RawTextHelpFormatter
,
466 parser
.add_argument('--soffice', metavar
='method:location', required
=True,
467 help="specify soffice instance to connect to\n"
468 "supported methods: 'path', 'connect'")
469 parser
.add_argument('--outdir', metavar
='URL', required
=True,
470 help="specify the output directory for flat ODF exports")
471 parser
.add_argument('--userdir', metavar
='URL',
472 help="specify user installation directory for 'path' method")
473 parser
.add_argument('--valgrind', action
='store_true',
474 help="pass --valgrind to soffice for 'path' method")
475 parser
.add_argument('dirs', metavar
='directory', nargs
='+')
477 args
= parser
.parse_args(argv
[1:])
482 if __name__
== "__main__":
483 opts
= parseArgs(sys
.argv
)
484 runHandleFileTests(opts
)
486 # vim:set shiftwidth=4 softtabstop=4 expandtab: