nss: upgrade to release 3.73
[LibreOffice.git] / bin / convwatch.py
blobd88d43d64a6f9a55b633eea17b404557d0d20ab1
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/.
9 # Conversion watch, initially intended to detect if document layout changed since the last time it was run.
11 # Print a set of docs, compare the pdf against the old run and highlight the differences
14 import getopt
15 import os
16 import subprocess
17 import sys
18 import time
19 import uuid
20 import datetime
21 import traceback
22 import threading
23 try:
24 from urllib.parse import quote
25 except ImportError:
26 from urllib import quote
28 try:
29 import pyuno
30 import uno
31 import unohelper
32 except ImportError:
33 print("pyuno not found: try to set PYTHONPATH and URE_BOOTSTRAP variables")
34 print("PYTHONPATH=/installation/opt/program")
35 print("URE_BOOTSTRAP=file:///installation/opt/program/fundamentalrc")
36 raise
38 try:
39 from com.sun.star.document import XDocumentEventListener
40 except ImportError:
41 print("UNO API class not found: try to set URE_BOOTSTRAP variable")
42 print("URE_BOOTSTRAP=file:///installation/opt/program/fundamentalrc")
43 raise
45 ### utilities ###
47 def log(*args):
48 print(*args, flush=True)
50 def partition(list, pred):
51 left = []
52 right = []
53 for e in list:
54 if pred(e):
55 left.append(e)
56 else:
57 right.append(e)
58 return (left, right)
60 def filelist(dir, suffix):
61 if len(dir) == 0:
62 raise Exception("filelist: empty directory")
63 if not(dir[-1] == "/"):
64 dir += "/"
65 files = [dir + f for f in os.listdir(dir)]
66 # log(files)
67 return [f for f in files
68 if os.path.isfile(f) and os.path.splitext(f)[1] == suffix]
70 def getFiles(dirs, suffix):
71 files = []
72 for dir in dirs:
73 files += filelist(dir, suffix)
74 return files
76 ### UNO utilities ###
78 class OfficeConnection:
79 def __init__(self, args):
80 self.args = args
81 self.soffice = None
82 self.socket = None
83 self.xContext = None
84 def setUp(self):
85 (method, sep, rest) = self.args["--soffice"].partition(":")
86 if sep != ":":
87 raise Exception("soffice parameter does not specify method")
88 if method == "path":
89 self.socket = "pipe,name=pytest" + str(uuid.uuid1())
90 try:
91 userdir = self.args["--userdir"]
92 except KeyError:
93 raise Exception("'path' method requires --userdir")
94 if not(userdir.startswith("file://")):
95 raise Exception("--userdir must be file URL")
96 self.soffice = self.bootstrap(rest, userdir, self.socket)
97 elif method == "connect":
98 self.socket = rest
99 else:
100 raise Exception("unsupported connection method: " + method)
101 self.xContext = self.connect(self.socket)
103 def bootstrap(self, soffice, userdir, socket):
104 argv = [ soffice, "--accept=" + socket + ";urp",
105 "-env:UserInstallation=" + userdir,
106 "--quickstart=no",
107 "--norestore", "--nologo", "--headless" ]
108 if "--valgrind" in self.args:
109 argv.append("--valgrind")
110 return subprocess.Popen(argv)
112 def connect(self, socket):
113 xLocalContext = uno.getComponentContext()
114 xUnoResolver = xLocalContext.ServiceManager.createInstanceWithContext(
115 "com.sun.star.bridge.UnoUrlResolver", xLocalContext)
116 url = "uno:" + socket + ";urp;StarOffice.ComponentContext"
117 log("OfficeConnection: connecting to: " + url)
118 while True:
119 try:
120 xContext = xUnoResolver.resolve(url)
121 return xContext
122 # except com.sun.star.connection.NoConnectException
123 except pyuno.getClass("com.sun.star.connection.NoConnectException"):
124 log("NoConnectException: sleeping...")
125 time.sleep(1)
127 def tearDown(self):
128 if self.soffice:
129 if self.xContext:
130 try:
131 log("tearDown: calling terminate()...")
132 xMgr = self.xContext.ServiceManager
133 xDesktop = xMgr.createInstanceWithContext(
134 "com.sun.star.frame.Desktop", self.xContext)
135 xDesktop.terminate()
136 log("...done")
137 # except com.sun.star.lang.DisposedException:
138 except pyuno.getClass("com.sun.star.beans.UnknownPropertyException"):
139 log("caught UnknownPropertyException")
140 pass # ignore, also means disposed
141 except pyuno.getClass("com.sun.star.lang.DisposedException"):
142 log("caught DisposedException")
143 pass # ignore
144 else:
145 self.soffice.terminate()
146 ret = self.soffice.wait()
147 self.xContext = None
148 self.socket = None
149 self.soffice = None
150 if ret != 0:
151 raise Exception("Exit status indicates failure: " + str(ret))
152 # return ret
154 class WatchDog(threading.Thread):
155 def __init__(self, connection):
156 threading.Thread.__init__(self, name="WatchDog " + connection.socket)
157 self.connection = connection
158 def run(self):
159 try:
160 if self.connection.soffice: # not possible for "connect"
161 self.connection.soffice.wait(timeout=120) # 2 minutes?
162 except subprocess.TimeoutExpired:
163 log("WatchDog: TIMEOUT -> killing soffice")
164 self.connection.soffice.terminate() # actually killing oosplash...
165 self.connection.xContext = None
166 log("WatchDog: killed soffice")
168 class PerTestConnection:
169 def __init__(self, args):
170 self.args = args
171 self.connection = None
172 self.watchdog = None
173 def getContext(self):
174 return self.connection.xContext
175 def setUp(self):
176 assert(not(self.connection))
177 def preTest(self):
178 conn = OfficeConnection(self.args)
179 conn.setUp()
180 self.connection = conn
181 self.watchdog = WatchDog(self.connection)
182 self.watchdog.start()
183 def postTest(self):
184 if self.connection:
185 try:
186 self.connection.tearDown()
187 finally:
188 self.connection = None
189 self.watchdog.join()
190 def tearDown(self):
191 assert(not(self.connection))
193 class PersistentConnection:
194 def __init__(self, args):
195 self.args = args
196 self.connection = None
197 def getContext(self):
198 return self.connection.xContext
199 def setUp(self):
200 conn = OfficeConnection(self.args)
201 conn.setUp()
202 self.connection = conn
203 def preTest(self):
204 assert(self.connection)
205 def postTest(self):
206 assert(self.connection)
207 def tearDown(self):
208 if self.connection:
209 try:
210 self.connection.tearDown()
211 finally:
212 self.connection = None
214 def simpleInvoke(connection, test):
215 try:
216 connection.preTest()
217 test.run(connection.getContext())
218 finally:
219 connection.postTest()
221 def retryInvoke(connection, test):
222 tries = 5
223 while tries > 0:
224 try:
225 tries -= 1
226 try:
227 connection.preTest()
228 test.run(connection.getContext())
229 return
230 finally:
231 connection.postTest()
232 except KeyboardInterrupt:
233 raise # Ctrl+C should work
234 except:
235 log("retryInvoke: caught exception")
236 raise Exception("FAILED retryInvoke")
238 def runConnectionTests(connection, invoker, tests):
239 try:
240 connection.setUp()
241 failed = []
242 for test in tests:
243 try:
244 invoker(connection, test)
245 except KeyboardInterrupt:
246 raise # Ctrl+C should work
247 except:
248 failed.append(test.file)
249 estr = traceback.format_exc()
250 log("... FAILED with exception:\n" + estr)
251 return failed
252 finally:
253 connection.tearDown()
255 class EventListener(XDocumentEventListener,unohelper.Base):
256 def __init__(self):
257 self.layoutFinished = False
258 def documentEventOccured(self, event):
259 # log(str(event.EventName))
260 if event.EventName == "OnLayoutFinished":
261 self.layoutFinished = True
262 def disposing(event):
263 pass
265 def mkPropertyValue(name, value):
266 return uno.createUnoStruct("com.sun.star.beans.PropertyValue",
267 name, 0, value, 0)
269 ### tests ###
271 def loadFromURL(xContext, url):
272 xDesktop = xContext.ServiceManager.createInstanceWithContext(
273 "com.sun.star.frame.Desktop", xContext)
274 props = [("Hidden", True), ("ReadOnly", True)] # FilterName?
275 loadProps = tuple([mkPropertyValue(name, value) for (name, value) in props])
276 xListener = EventListener()
277 xGEB = xContext.getValueByName(
278 "/singletons/com.sun.star.frame.theGlobalEventBroadcaster")
279 xGEB.addDocumentEventListener(xListener)
280 xDoc = None
281 try:
282 xDoc = xDesktop.loadComponentFromURL(url, "_blank", 0, loadProps)
283 if xDoc is None:
284 raise Exception("No document loaded?")
285 time_ = 0
286 while time_ < 30:
287 if xListener.layoutFinished:
288 return xDoc
289 log("delaying...")
290 time_ += 1
291 time.sleep(1)
292 log("timeout: no OnLayoutFinished received")
293 return xDoc
294 except:
295 if xDoc:
296 log("CLOSING")
297 xDoc.close(True)
298 raise
299 finally:
300 if xListener:
301 xGEB.removeDocumentEventListener(xListener)
303 def printDoc(xContext, xDoc, url):
304 props = [ mkPropertyValue("FileName", url) ]
305 # xDoc.print(props)
306 uno.invoke(xDoc, "print", (tuple(props),)) # damn, that's a keyword!
307 busy = True
308 while busy:
309 log("printing...")
310 time.sleep(1)
311 prt = xDoc.getPrinter()
312 for value in prt:
313 if value.Name == "IsBusy":
314 busy = value.Value
315 log("...done printing")
317 class LoadPrintFileTest:
318 def __init__(self, file, prtsuffix):
319 self.file = file
320 self.prtsuffix = prtsuffix
321 def run(self, xContext):
322 start = datetime.datetime.now()
323 log("Time: " + str(start) + " Loading document: " + self.file)
324 xDoc = None
325 try:
326 if os.name == 'nt' and self.file[1] == ':':
327 url = "file:///" + self.file[0:2] + quote(self.file[2:])
328 else:
329 url = "file://" + quote(self.file)
330 xDoc = loadFromURL(xContext, url)
331 printDoc(xContext, xDoc, url + self.prtsuffix)
332 finally:
333 if xDoc:
334 xDoc.close(True)
335 end = datetime.datetime.now()
336 log("...done with: " + self.file + " in: " + str(end - start))
338 def runLoadPrintFileTests(opts, dirs, suffix, reference):
339 if reference:
340 prtsuffix = ".pdf.reference"
341 else:
342 prtsuffix = ".pdf"
343 files = getFiles(dirs, suffix)
344 tests = (LoadPrintFileTest(file, prtsuffix) for file in files)
345 # connection = PersistentConnection(opts)
346 connection = PerTestConnection(opts)
347 failed = runConnectionTests(connection, simpleInvoke, tests)
348 print("all printed: FAILURES: " + str(len(failed)))
349 for fail in failed:
350 print(fail)
351 return failed
353 def mkImages(file, resolution):
354 argv = [ "gs", "-r" + resolution, "-sOutputFile=" + file + ".%04d.jpeg",
355 "-dNOPROMPT", "-dNOPAUSE", "-dBATCH", "-sDEVICE=jpeg", file ]
356 ret = subprocess.check_call(argv)
358 def mkAllImages(dirs, suffix, resolution, reference, failed):
359 if reference:
360 prtsuffix = ".pdf.reference"
361 else:
362 prtsuffix = ".pdf"
363 for dir in dirs:
364 files = filelist(dir, suffix)
365 log(files)
366 for f in files:
367 if f in failed:
368 log("Skipping failed: " + f)
369 else:
370 mkImages(f + prtsuffix, resolution)
372 def identify(imagefile):
373 argv = ["identify", "-format", "%k", imagefile]
374 process = subprocess.Popen(argv, stdout=subprocess.PIPE)
375 result, _ = process.communicate()
376 if process.wait() != 0:
377 raise Exception("identify failed")
378 if result.partition(b"\n")[0] != b"1":
379 log("identify result: " + result.decode('utf-8'))
380 log("DIFFERENCE in " + imagefile)
382 def compose(refimagefile, imagefile, diffimagefile):
383 argv = [ "composite", "-compose", "difference",
384 refimagefile, imagefile, diffimagefile ]
385 subprocess.check_call(argv)
387 def compareImages(file):
388 allimages = [f for f in filelist(os.path.dirname(file), ".jpeg")
389 if f.startswith(file)]
390 # refimages = [f for f in filelist(os.path.dirname(file), ".jpeg")
391 # if f.startswith(file + ".reference")]
392 # log("compareImages: allimages:" + str(allimages))
393 (refimages, images) = partition(sorted(allimages),
394 lambda f: f.startswith(file + ".pdf.reference"))
395 # log("compareImages: images" + str(images))
396 for (image, refimage) in zip(images, refimages):
397 compose(image, refimage, image + ".diff")
398 identify(image + ".diff")
399 if (len(images) != len(refimages)):
400 log("DIFFERENT NUMBER OF IMAGES FOR: " + file)
402 def compareAllImages(dirs, suffix):
403 log("compareAllImages...")
404 for dir in dirs:
405 files = filelist(dir, suffix)
406 # log("compareAllImages:" + str(files))
407 for f in files:
408 compareImages(f)
409 log("...compareAllImages done")
412 def parseArgs(argv):
413 (optlist,args) = getopt.getopt(argv[1:], "hr",
414 ["help", "soffice=", "userdir=", "reference", "valgrind"])
415 # print optlist
416 return (dict(optlist), args)
418 def usage():
419 message = """usage: {program} [option]... [directory]..."
420 -h | --help: print usage information
421 -r | --reference: generate new reference files (otherwise: compare)
422 --soffice=method:location
423 specify soffice instance to connect to
424 supported methods: 'path', 'connect'
425 --userdir=URL specify user installation directory for 'path' method
426 --valgrind pass --valgrind to soffice for 'path' method"""
427 print(message.format(program = os.path.basename(sys.argv[0])))
429 def checkTools():
430 try:
431 subprocess.check_output(["gs", "--version"])
432 except:
433 print("Cannot execute 'gs'. Please install ghostscript.")
434 sys.exit(1)
435 try:
436 subprocess.check_output(["composite", "-version"])
437 subprocess.check_output(["identify", "-version"])
438 except:
439 print("Cannot execute 'composite' or 'identify'.")
440 print("Please install ImageMagick.")
441 sys.exit(1)
443 if __name__ == "__main__":
444 checkTools()
445 (opts,args) = parseArgs(sys.argv)
446 if len(args) == 0:
447 usage()
448 sys.exit(1)
449 if "-h" in opts or "--help" in opts:
450 usage()
451 sys.exit()
452 elif "--soffice" in opts:
453 reference = "-r" in opts or "--reference" in opts
454 failed = runLoadPrintFileTests(opts, args, ".odt", reference)
455 mkAllImages(args, ".odt", "200", reference, failed)
456 if not(reference):
457 compareAllImages(args, ".odt")
458 else:
459 usage()
460 sys.exit(1)
462 # vim: set shiftwidth=4 softtabstop=4 expandtab: