1 # -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
3 # This file is part of the LibreOffice project.
5 # This Source Code Form is subject to the terms of the Mozilla Public
6 # License, v. 2.0. If a copy of the MPL was not distributed with this
7 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
17 from urllib
.parse
import quote
19 from urllib
import quote
26 print("pyuno not found: try to set PYTHONPATH and URE_BOOTSTRAP variables")
27 print("PYTHONPATH=/installation/opt/program")
28 print("URE_BOOTSTRAP=file:///installation/opt/program/fundamentalrc")
32 from com
.sun
.star
.document
import XDocumentEventListener
34 print("UNO API class not found: try to set URE_BOOTSTRAP variable")
35 print("URE_BOOTSTRAP=file:///installation/opt/program/fundamentalrc")
40 def partition(list, pred
):
50 def filelist(dir, suffix
):
52 raise Exception("filelist: empty directory")
53 if not(dir[-1] == "/"):
55 files
= [dir + f
for f
in os
.listdir(dir)]
57 return [f
for f
in files
58 if os
.path
.isfile(f
) and os
.path
.splitext(f
)[1] == suffix
]
60 def getFiles(dirs
, suffix
):
63 files
+= filelist(dir, suffix
)
68 class OfficeConnection
:
69 def __init__(self
, args
):
75 (method
, sep
, rest
) = self
.args
["--soffice"].partition(":")
77 raise Exception("soffice parameter does not specify method")
79 socket
= "pipe,name=pytest" + str(uuid
.uuid1())
81 userdir
= self
.args
["--userdir"]
83 raise Exception("'path' method requires --userdir")
84 if not(userdir
.startswith("file://")):
85 raise Exception("--userdir must be file URL")
86 self
.soffice
= self
.bootstrap(rest
, userdir
, socket
)
87 elif method
== "connect":
90 raise Exception("unsupported connection method: " + method
)
91 self
.xContext
= self
.connect(socket
)
93 def bootstrap(self
, soffice
, userdir
, socket
):
94 argv
= [ soffice
, "--accept=" + socket
+ ";urp",
95 "-env:UserInstallation=" + userdir
,
96 "--quickstart=no", "--nofirststartwizard",
97 "--norestore", "--nologo", "--headless" ]
98 if "--valgrind" in self
.args
:
99 argv
.append("--valgrind")
100 return subprocess
.Popen(argv
)
102 def connect(self
, socket
):
103 xLocalContext
= uno
.getComponentContext()
104 xUnoResolver
= xLocalContext
.ServiceManager
.createInstanceWithContext(
105 "com.sun.star.bridge.UnoUrlResolver", xLocalContext
)
106 url
= "uno:" + socket
+ ";urp;StarOffice.ComponentContext"
107 print("OfficeConnection: connecting to: " + url
)
110 xContext
= xUnoResolver
.resolve(url
)
112 # except com.sun.star.connection.NoConnectException
113 except pyuno
.getClass("com.sun.star.connection.NoConnectException"):
114 print("NoConnectException: sleeping...")
121 print("tearDown: calling terminate()...")
122 xMgr
= self
.xContext
.ServiceManager
123 xDesktop
= xMgr
.createInstanceWithContext(
124 "com.sun.star.frame.Desktop", self
.xContext
)
127 # except com.sun.star.lang.DisposedException:
128 except pyuno
.getClass("com.sun.star.beans.UnknownPropertyException"):
129 print("caught UnknownPropertyException")
130 pass # ignore, also means disposed
131 except pyuno
.getClass("com.sun.star.lang.DisposedException"):
132 print("caught DisposedException")
135 self
.soffice
.terminate()
136 ret
= self
.soffice
.wait()
141 raise Exception("Exit status indicates failure: " + str(ret
))
144 class PerTestConnection
:
145 def __init__(self
, args
):
147 self
.connection
= None
148 def getContext(self
):
149 return self
.connection
.xContext
151 assert(not(self
.connection
))
153 conn
= OfficeConnection(self
.args
)
155 self
.connection
= conn
159 self
.connection
.tearDown()
161 self
.connection
= None
163 assert(not(self
.connection
))
165 class PersistentConnection
:
166 def __init__(self
, args
):
168 self
.connection
= None
169 def getContext(self
):
170 return self
.connection
.xContext
172 conn
= OfficeConnection(self
.args
)
174 self
.connection
= conn
176 assert(self
.connection
)
178 assert(self
.connection
)
182 self
.connection
.tearDown()
184 self
.connection
= None
186 def simpleInvoke(connection
, test
):
189 test
.run(connection
.getContext())
191 connection
.postTest()
193 def retryInvoke(connection
, test
):
200 test
.run(connection
.getContext())
203 connection
.postTest()
204 except KeyboardInterrupt:
205 raise # Ctrl+C should work
207 print("retryInvoke: caught exception")
208 raise Exception("FAILED retryInvoke")
210 def runConnectionTests(connection
, invoker
, tests
):
214 invoker(connection
, test
)
216 connection
.tearDown()
218 class EventListener(XDocumentEventListener
,unohelper
.Base
):
220 self
.layoutFinished
= False
221 def documentEventOccured(self
, event
):
222 # print(str(event.EventName))
223 if event
.EventName
== "OnLayoutFinished":
224 self
.layoutFinished
= True
225 def disposing(event
):
228 def mkPropertyValue(name
, value
):
229 return uno
.createUnoStruct("com.sun.star.beans.PropertyValue",
234 def loadFromURL(xContext
, url
):
235 xDesktop
= xContext
.ServiceManager
.createInstanceWithContext(
236 "com.sun.star.frame.Desktop", xContext
)
237 props
= [("Hidden", True), ("ReadOnly", True)] # FilterName?
238 loadProps
= tuple([mkPropertyValue(name
, value
) for (name
, value
) in props
])
239 xListener
= EventListener()
240 xGEB
= xContext
.ServiceManager
.createInstanceWithContext(
241 "com.sun.star.frame.GlobalEventBroadcaster", xContext
)
242 xGEB
.addDocumentEventListener(xListener
)
244 xDoc
= xDesktop
.loadComponentFromURL(url
, "_blank", 0, loadProps
)
247 if xListener
.layoutFinished
:
252 print("timeout: no OnLayoutFinished received")
261 xGEB
.removeDocumentEventListener(xListener
)
263 def printDoc(xContext
, xDoc
, url
):
264 props
= [ mkPropertyValue("FileName", url
) ]
266 uno
.invoke(xDoc
, "print", (tuple(props
),)) # damn, that's a keyword!
271 prt
= xDoc
.getPrinter()
273 if value
.Name
== "IsBusy":
275 print("...done printing")
277 class LoadPrintFileTest
:
278 def __init__(self
, file, prtsuffix
):
280 self
.prtsuffix
= prtsuffix
281 def run(self
, xContext
):
282 print("Loading document: " + self
.file)
284 url
= "file://" + quote(self
.file)
285 xDoc
= loadFromURL(xContext
, url
)
286 printDoc(xContext
, xDoc
, url
+ self
.prtsuffix
)
290 print("...done with: " + self
.file)
292 def runLoadPrintFileTests(opts
, dirs
, suffix
, reference
):
294 prtsuffix
= ".pdf.reference"
297 files
= getFiles(dirs
, suffix
)
298 tests
= (LoadPrintFileTest(file, prtsuffix
) for file in files
)
299 connection
= PersistentConnection(opts
)
300 # connection = PerTestConnection(opts)
301 runConnectionTests(connection
, simpleInvoke
, tests
)
303 def mkImages(file, resolution
):
304 argv
= [ "gs", "-r" + resolution
, "-sOutputFile=" + file + ".%04d.jpeg",
305 "-dNOPROMPT", "-dNOPAUSE", "-dBATCH", "-sDEVICE=jpeg", file ]
306 ret
= subprocess
.check_call(argv
)
308 def mkAllImages(dirs
, suffix
, resolution
, reference
):
310 prtsuffix
= ".pdf.reference"
314 files
= filelist(dir, suffix
)
317 mkImages(f
+ prtsuffix
, resolution
)
319 def identify(imagefile
):
320 argv
= ["identify", "-format", "%k", imagefile
]
321 process
= subprocess
.Popen(argv
, stdout
=subprocess
.PIPE
)
322 result
, _
= process
.communicate()
323 if process
.wait() != 0:
324 raise Exception("identify failed")
325 if result
.partition(b
"\n")[0] != b
"1":
326 print("identify result: " + result
)
327 print("DIFFERENCE in " + imagefile
)
329 def compose(refimagefile
, imagefile
, diffimagefile
):
330 argv
= [ "composite", "-compose", "difference",
331 refimagefile
, imagefile
, diffimagefile
]
332 subprocess
.check_call(argv
)
334 def compareImages(file):
335 allimages
= [f
for f
in filelist(os
.path
.dirname(file), ".jpeg")
336 if f
.startswith(file)]
337 # refimages = [f for f in filelist(os.path.dirname(file), ".jpeg")
338 # if f.startswith(file + ".reference")]
339 # print("compareImages: allimages:" + str(allimages))
340 (refimages
, images
) = partition(sorted(allimages
),
341 lambda f
: f
.startswith(file + ".pdf.reference"))
342 # print("compareImages: images" + str(images))
343 for (image
, refimage
) in zip(images
, refimages
):
344 compose(image
, refimage
, image
+ ".diff")
345 identify(image
+ ".diff")
346 if (len(images
) != len(refimages
)):
347 print("DIFFERENT NUMBER OF IMAGES FOR: " + file)
349 def compareAllImages(dirs
, suffix
):
350 print("compareAllImages...")
352 files
= filelist(dir, suffix
)
353 # print("compareAllImages:" + str(files))
356 print("...compareAllImages done")
360 (optlist
,args
) = getopt
.getopt(argv
[1:], "hr",
361 ["help", "soffice=", "userdir=", "reference", "valgrind"])
363 return (dict(optlist
), args
)
366 message
= """usage: {program} [option]... [directory]..."
367 -h | --help: print usage information
368 -r | --reference: generate new reference files (otherwise: compare)
369 --soffice=method:location
370 specify soffice instance to connect to
371 supported methods: 'path', 'connect'
372 --userdir=URL specify user installation directory for 'path' method
373 --valgrind pass --valgrind to soffice for 'path' method"""
374 print(message
.format(program
= os
.path
.basename(sys
.argv
[0])))
378 subprocess
.check_output(["gs", "--version"])
380 print("Cannot execute 'gs'. Please install ghostscript.")
383 subprocess
.check_output(["composite", "-version"])
384 subprocess
.check_output(["identify", "-version"])
386 print("Cannot execute 'composite' or 'identify'.")
387 print("Please install ImageMagick.")
390 if __name__
== "__main__":
392 (opts
,args
) = parseArgs(sys
.argv
)
396 if "-h" in opts
or "--help" in opts
:
399 elif "--soffice" in opts
:
400 reference
= "-r" in opts
or "--reference" in opts
401 runLoadPrintFileTests(opts
, args
, ".odt", reference
)
402 mkAllImages(args
, ".odt", "200", reference
)
404 compareAllImages(args
, ".odt")
409 # vim:set shiftwidth=4 softtabstop=4 expandtab: