Reduce number of clipping paths created during PDF import
[inkscape.git] / buildtools / lpetest-parse.py
blob1b9efb73116accfa0c65cf52945484658984f1fd
1 #!/usr/bin/env python3
2 # coding=utf-8
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # Script to evaluate LPE test output and update reference files.
6 #
7 # Copyright (C) 2023-2024 Authors
8 #
9 # Authors:
10 # PBS <pbs3141@gmail.com>
11 # Martin Owens <doctormo@gmail.com>
13 # Released under GNU GPL v2+, read the file 'COPYING' for more information.
15 import os, re, sys, argparse
17 description = '''Evaluate LPE test output and update reference files.
19 Instructions:
20 1. Download the "complete raw" job log from one of the test pipelines and place it in the same directory as this script, named joblog.txt. Alternatively, collect the output from running the test suite.
21 2. Run the script and examine the output.
22 3. If there are no regressions, say yes when it offers to update the testfiles/lpe_tests/* files.
24 Output format:
25 The output consists of the old paths 'a.txt', new paths 'b.txt', and visual comparison 'path-comparison.svg'.
26 For each LPE test failure,
27 - The old path is written on a single line to 'a.txt'.
28 - The new path is written on a single line to 'b.txt'.
29 - A group is written to 'path-comparison.svg' containing the old path in yellow and the new path in cyan,
30 with additive blend mode. Non-matching areas therefore appear in either yellow or cyan.'''
32 open_testfiles = {}
34 def yesnoprompt():
35 while True:
36 txt = input("[y/n]: ").lower()
37 if txt in ('y', 'yes'): return True
38 if txt in ('n', 'no'): return False
39 sys.stdout.write("\nPlease specify yes or no.\n")
41 def update_reference_files(path, a, b, svg, id):
42 id = id.split("(")[0]
43 m = re.search("testfiles", svg)
44 if m is None:
45 sys.stderr.write(f"Warning: Ignoring file '{svg}'\n")
46 return
48 # Open and cache the contents of the file
49 name = os.path.join(path, svg[m.start():])
50 if name not in open_testfiles:
51 with open(name, "r") as tmpf:
52 open_testfiles[name] = tmpf.read()
54 contents = open_testfiles[name]
55 m = re.search(fr'\bid *= *"{id}"', contents)
56 if m is None:
57 sys.stderr.write(f"Warning: Ignoring id {id}\n")
58 return
60 i = max(
61 contents.rfind("<path", 0, m.start()),
62 contents.rfind("<ellipse", 0, m.start())
64 if i < 0:
65 sys.stderr.write(f"Warning: Couldn't find start of path for {id}\n")
66 return
68 m = re.compile(r'\bd *= *"').search(contents, i)
69 if m is None:
70 sys.stderr.write(f"Warning: Couldn't find d attribute for {id}\n")
71 return
73 i = m.end()
74 j = contents.find('"', i)
75 if j == -1:
76 sys.stderr.write(f"Warning: Couldn't find end of d attribute for {id}\n")
77 return
79 contents = contents[:i] + b + contents[j:]
80 open_testfiles[name] = contents
82 def found(cmpf, af, bf, inkscape, count, a, b, svg, id):
83 cmpf.write(f""" <g>
84 <path style="fill:#ffff00;stroke:none" d="{a}" id="good{count}" />
85 <path style="fill:#00ffff;stroke:none;mix-blend-mode:lighten" d="{b}" id="bad{count}" />
86 </g>
87 """)
88 af.write(f"{a}\n")
89 bf.write(f"{b}\n")
90 update_reference_files(inkscape, a, b, svg, id)
92 def main():
93 parser = argparse.ArgumentParser(prog='lpetest-parse.py', description=description, formatter_class=argparse.RawTextHelpFormatter)
94 parser.add_argument('--log', default='joblog.txt', metavar='FILE', help='Input file containing test log')
95 parser.add_argument('--cmp', default='path-comparison.svg', metavar='FILE', help='Output file for visualisation of path differences')
96 parser.add_argument('--a', default='a.txt', metavar='FILE', help='Output file for original paths')
97 parser.add_argument('--b', default='b.txt', metavar='FILE', help='Output file for new paths')
98 parser.add_argument('--inkscape', default='..', metavar='DIR', help='Inkscape project root directory')
99 args = parser.parse_args()
101 if not os.path.isdir(os.path.join(args.inkscape, '.git')):
102 sys.stderr.write(f"Project not found in '{args.inkscape}'\n\n")
103 sys.stderr.write("Please run this from the Inkscape project root directory, or pass it as --inkscape\n")
104 sys.exit(-1)
106 if not os.path.isfile(args.log):
107 sys.stderr.write(f"Test log not found in '{args.log}'\n\n")
108 sys.stderr.write("Please run the suite or download log file from a CI pipeline first\n")
109 sys.exit(-2)
111 with open(args.log, "r") as logf:
112 log = logf.read()
114 with open(args.cmp, "w") as cmpf, open(args.a, "w") as af, open(args.b, "w") as bf:
115 cmpf.write("<svg>\n")
117 data = {}
118 count = 0
119 for tag, value in re.findall(r"\s*\d+:\s+(?P<name>svg|id|a|b):\s*\d+:\s+(?P<value>.+)", log):
120 data[tag] = value
121 if tag == "b":
122 count += 1
123 found(cmpf, af, bf, args.inkscape, count, **data)
125 cmpf.write("</svg>\n")
127 if len(open_testfiles) > 0:
128 print("Overwrite these files?")
129 for name in open_testfiles.keys():
130 print(name)
132 if yesnoprompt():
133 for name, contents in open_testfiles.items():
134 with open(name, "w") as out:
135 out.write(contents)
137 if __name__ == "__main__":
138 main()