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
,
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
.getValueByName(
241 "/singletons/com.sun.star.frame.theGlobalEventBroadcaster")
242 xGEB
.addDocumentEventListener(xListener
)
245 xDoc
= xDesktop
.loadComponentFromURL(url
, "_blank", 0, loadProps
)
247 raise Exception("No document loaded?")
250 if xListener
.layoutFinished
:
255 print("timeout: no OnLayoutFinished received")
264 xGEB
.removeDocumentEventListener(xListener
)
266 def printDoc(xContext
, xDoc
, url
):
267 props
= [ mkPropertyValue("FileName", url
) ]
269 uno
.invoke(xDoc
, "print", (tuple(props
),)) # damn, that's a keyword!
274 prt
= xDoc
.getPrinter()
276 if value
.Name
== "IsBusy":
278 print("...done printing")
280 class LoadPrintFileTest
:
281 def __init__(self
, file, prtsuffix
):
283 self
.prtsuffix
= prtsuffix
284 def run(self
, xContext
):
285 print("Loading document: " + self
.file)
288 url
= "file://" + quote(self
.file)
289 xDoc
= loadFromURL(xContext
, url
)
290 printDoc(xContext
, xDoc
, url
+ self
.prtsuffix
)
294 print("...done with: " + self
.file)
296 def runLoadPrintFileTests(opts
, dirs
, suffix
, reference
):
298 prtsuffix
= ".pdf.reference"
301 files
= getFiles(dirs
, suffix
)
302 tests
= (LoadPrintFileTest(file, prtsuffix
) for file in files
)
303 connection
= PersistentConnection(opts
)
304 # connection = PerTestConnection(opts)
305 runConnectionTests(connection
, simpleInvoke
, tests
)
307 def mkImages(file, resolution
):
308 argv
= [ "gs", "-r" + resolution
, "-sOutputFile=" + file + ".%04d.jpeg",
309 "-dNOPROMPT", "-dNOPAUSE", "-dBATCH", "-sDEVICE=jpeg", file ]
310 ret
= subprocess
.check_call(argv
)
312 def mkAllImages(dirs
, suffix
, resolution
, reference
):
314 prtsuffix
= ".pdf.reference"
318 files
= filelist(dir, suffix
)
321 mkImages(f
+ prtsuffix
, resolution
)
323 def identify(imagefile
):
324 argv
= ["identify", "-format", "%k", imagefile
]
325 process
= subprocess
.Popen(argv
, stdout
=subprocess
.PIPE
)
326 result
, _
= process
.communicate()
327 if process
.wait() != 0:
328 raise Exception("identify failed")
329 if result
.partition(b
"\n")[0] != b
"1":
330 print("identify result: " + result
.decode('utf-8'))
331 print("DIFFERENCE in " + imagefile
)
333 def compose(refimagefile
, imagefile
, diffimagefile
):
334 argv
= [ "composite", "-compose", "difference",
335 refimagefile
, imagefile
, diffimagefile
]
336 subprocess
.check_call(argv
)
338 def compareImages(file):
339 allimages
= [f
for f
in filelist(os
.path
.dirname(file), ".jpeg")
340 if f
.startswith(file)]
341 # refimages = [f for f in filelist(os.path.dirname(file), ".jpeg")
342 # if f.startswith(file + ".reference")]
343 # print("compareImages: allimages:" + str(allimages))
344 (refimages
, images
) = partition(sorted(allimages
),
345 lambda f
: f
.startswith(file + ".pdf.reference"))
346 # print("compareImages: images" + str(images))
347 for (image
, refimage
) in zip(images
, refimages
):
348 compose(image
, refimage
, image
+ ".diff")
349 identify(image
+ ".diff")
350 if (len(images
) != len(refimages
)):
351 print("DIFFERENT NUMBER OF IMAGES FOR: " + file)
353 def compareAllImages(dirs
, suffix
):
354 print("compareAllImages...")
356 files
= filelist(dir, suffix
)
357 # print("compareAllImages:" + str(files))
360 print("...compareAllImages done")
364 (optlist
,args
) = getopt
.getopt(argv
[1:], "hr",
365 ["help", "soffice=", "userdir=", "reference", "valgrind"])
367 return (dict(optlist
), args
)
370 message
= """usage: {program} [option]... [directory]..."
371 -h | --help: print usage information
372 -r | --reference: generate new reference files (otherwise: compare)
373 --soffice=method:location
374 specify soffice instance to connect to
375 supported methods: 'path', 'connect'
376 --userdir=URL specify user installation directory for 'path' method
377 --valgrind pass --valgrind to soffice for 'path' method"""
378 print(message
.format(program
= os
.path
.basename(sys
.argv
[0])))
382 subprocess
.check_output(["gs", "--version"])
384 print("Cannot execute 'gs'. Please install ghostscript.")
387 subprocess
.check_output(["composite", "-version"])
388 subprocess
.check_output(["identify", "-version"])
390 print("Cannot execute 'composite' or 'identify'.")
391 print("Please install ImageMagick.")
394 if __name__
== "__main__":
396 (opts
,args
) = parseArgs(sys
.argv
)
400 if "-h" in opts
or "--help" in opts
:
403 elif "--soffice" in opts
:
404 reference
= "-r" in opts
or "--reference" in opts
405 runLoadPrintFileTests(opts
, args
, ".odt", reference
)
406 mkAllImages(args
, ".odt", "200", reference
)
408 compareAllImages(args
, ".odt")
413 # vim:set shiftwidth=4 softtabstop=4 expandtab: