tdf#137335 calculate paragraph height in RTF/DOCX
[LibreOffice.git] / bin / update_pch_bisect
blob71072303efdc9ad92bf48f12aec0326843c01eb6
1 #! /usr/bin/env python
2 # -*- Mode: python; tab-width: 4; indent-tabs-mode: t -*-
4 # This file is part of the LibreOffice project.
6 # This Source Code Form is subject to the terms of the Mozilla Public
7 # License, v. 2.0. If a copy of the MPL was not distributed with this
8 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
11 """
12 This script is to fix precompiled headers.
14 This script runs in two modes.
15 In one mode, it starts with a header
16 that doesn't compile. If finds the
17 minimum number of includes in the
18 header to remove to get a successful
19 run of the command (i.e. compile).
21 In the second mode, it starts with a
22 header that compiles fine, however,
23 it contains one or more required
24 include without which it wouldn't
25 compile, which it identifies.
27 Usage: ./bin/update_pch_bisect ./vcl/inc/pch/precompiled_vcl.hxx "make vcl.build" --find-required --verbose
28 """
30 import sys
31 import re
32 import os
33 import unittest
34 import subprocess
36 SILENT = True
37 FIND_CONFLICTS = True
39 IGNORE = 0
40 GOOD = 1
41 TEST_ON = 2
42 TEST_OFF = 3
43 BAD = 4
45 def run(command):
46 try:
47 cmd = command.split(' ', 1)
48 status = subprocess.call(cmd, stdout=open(os.devnull, 'w'),
49 stderr=subprocess.STDOUT, close_fds=True)
50 return True if status == 0 else False
51 except Exception as e:
52 sys.stderr.write('Error: {}\n'.format(e))
53 return False
55 def update_pch(filename, lines, marks):
56 with open(filename, 'w') as f:
57 for i, mark in enumerate(marks):
58 if mark <= TEST_ON:
59 f.write(lines[i])
60 else:
61 f.write('//' + lines[i])
63 def log(*args, **kwargs):
64 global SILENT
65 if not SILENT:
66 print(*args, **kwargs)
68 def bisect(lines, marks, min, max, update, command):
69 """ Disable half the includes and
70 calls the command.
71 Depending on the result,
72 recurse or return.
73 """
74 global FIND_CONFLICTS
76 log('Bisecting [{}, {}].'.format(min+1, max))
77 for i in range(min, max):
78 if marks[i] != IGNORE:
79 marks[i] = TEST_ON if FIND_CONFLICTS else TEST_OFF
81 assume_fail = False
82 if not FIND_CONFLICTS:
83 on_list = [x for x in marks if x in (TEST_ON, GOOD)]
84 assume_fail = (len(on_list) == 0)
86 update(lines, marks)
87 if assume_fail or not command():
88 # Failed
89 log('Failed [{}, {}].'.format(min+1, max))
90 if min >= max - 1:
91 if not FIND_CONFLICTS:
92 # Try with this one alone.
93 marks[min] = TEST_ON
94 update(lines, marks)
95 if command():
96 log(' Found @{}: {}'.format(min+1, lines[min].strip('\n')))
97 marks[min] = GOOD
98 return marks
99 else:
100 log(' Found @{}: {}'.format(min+1, lines[min].strip('\n')))
101 # Either way, this one is irrelevant.
102 marks[min] = BAD
103 return marks
105 # Bisect
106 for i in range(min, max):
107 if marks[i] != IGNORE:
108 marks[i] = TEST_OFF if FIND_CONFLICTS else TEST_ON
110 half = min + ((max - min) / 2)
111 marks = bisect(lines, marks, min, half, update, command)
112 marks = bisect(lines, marks, half, max, update, command)
113 else:
114 # Success
115 if FIND_CONFLICTS:
116 log(' Good [{}, {}].'.format(min+1, max))
117 for i in range(min, max):
118 if marks[i] != IGNORE:
119 marks[i] = GOOD
121 return marks
123 def get_filename(line):
124 """ Strips the line from the
125 '#include' and angled brackets
126 and return the filename only.
128 return re.sub(r'(.*#include\s*)<(.*)>(.*)', r'\2', line)
130 def get_marks(lines):
131 marks = []
132 min = -1
133 max = -1
134 for i, line in enumerate(lines):
135 if line.startswith('#include'):
136 marks.append(TEST_ON)
137 min = i if min < 0 else min
138 max = i
139 else:
140 marks.append(IGNORE)
142 return (marks, min, max+1)
144 def main():
146 global FIND_CONFLICTS
147 global SILENT
149 filename = sys.argv[1]
150 command = sys.argv[2]
152 for i in range(3, len(sys.argv)):
153 opt = sys.argv[i]
154 if opt == '--find-conflicts':
155 FIND_CONFLICTS = True
156 elif opt == '--find-required':
157 FIND_CONFLICTS = False
158 elif opt == '--verbose':
159 SILENT = False
160 else:
161 sys.stderr.write('Error: Unknown option [{}].\n'.format(opt))
162 return 1
164 lines = []
165 with open(filename) as f:
166 lines = f.readlines()
168 (marks, min, max) = get_marks(lines)
170 # Test preconditions.
171 log('Validating all-excluded state...')
172 for i in range(min, max):
173 if marks[i] != IGNORE:
174 marks[i] = TEST_OFF
175 update_pch(filename, lines, marks)
176 res = run(command)
178 if FIND_CONFLICTS:
179 # Must build all excluded.
180 if not res:
181 sys.stderr.write("Error: broken state when all excluded, fix first and try again.")
182 return 1
183 else:
184 # If builds all excluded, we can't bisect.
185 if res:
186 sys.stderr.write("Done: in good state when all excluded, nothing to do.")
187 return 1
189 # Must build all included.
190 log('Validating all-included state...')
191 for i in range(min, max):
192 if marks[i] != IGNORE:
193 marks[i] = TEST_ON
194 update_pch(filename, lines, marks)
195 if not run(command):
196 sys.stderr.write("Error: broken state without modifying, fix first and try again.")
197 return 1
199 marks = bisect(lines, marks, min, max+1,
200 lambda l, m: update_pch(filename, l, m),
201 lambda: run(command))
202 if not FIND_CONFLICTS:
203 # Simplify further, as sometimes we can have
204 # false positives due to the benign nature
205 # of includes that are not absolutely required.
206 for i, mark in enumerate(marks):
207 if mark == GOOD:
208 marks[i] = TEST_OFF
209 update_pch(filename, lines, marks)
210 if not run(command):
211 # Revert.
212 marks[i] = GOOD
213 else:
214 marks[i] = BAD
215 elif mark == TEST_OFF:
216 marks[i] = TEST_ON
218 update_pch(filename, lines, marks)
220 log('')
221 for i, mark in enumerate(marks):
222 if mark == (BAD if FIND_CONFLICTS else GOOD):
223 print("'{}',".format(get_filename(lines[i].strip('\n'))))
225 return 0
227 if __name__ == '__main__':
229 if len(sys.argv) in (3, 4, 5):
230 status = main()
231 sys.exit(status)
233 print('Usage: {} <pch> <command> [--find-conflicts]|[--find-required] [--verbose]\n'.format(sys.argv[0]))
234 print(' --find-conflicts - Finds all conflicting includes. (Default)')
235 print(' Must compile without any includes.\n')
236 print(' --find-required - Finds all required includes.')
237 print(' Must compile with all includes.\n')
238 print(' --verbose - print noisy progress.')
239 print('Example: ./bin/update_pch_bisect ./vcl/inc/pch/precompiled_vcl.hxx "make vcl.build" --find-required --verbose')
240 print('\nRunning unit-tests...')
243 class TestBisectConflict(unittest.TestCase):
244 TEST = """ /* Test header. */
245 #include <memory>
246 #include <set>
247 #include <algorithm>
248 #include <vector>
249 /* blah blah */
251 BAD_LINE = "#include <bad>"
253 def setUp(self):
254 global FIND_CONFLICTS
255 FIND_CONFLICTS = True
257 def _update_func(self, lines, marks):
258 self.lines = []
259 for i, mark in enumerate(marks):
260 if mark <= TEST_ON:
261 self.lines.append(lines[i])
262 else:
263 self.lines.append('//' + lines[i])
265 def _test_func(self):
266 """ Command function called by bisect.
267 Returns True on Success, False on failure.
269 # If the bad line is still there, fail.
270 return self.BAD_LINE not in self.lines
272 def test_success(self):
273 lines = self.TEST.split('\n')
274 (marks, min, max) = get_marks(lines)
275 marks = bisect(lines, marks, min, max,
276 lambda l, m: self._update_func(l, m),
277 lambda: self._test_func())
278 self.assertTrue(BAD not in marks)
280 def test_conflict(self):
281 lines = self.TEST.split('\n')
282 for pos in range(len(lines) + 1):
283 lines = self.TEST.split('\n')
284 lines.insert(pos, self.BAD_LINE)
285 (marks, min, max) = get_marks(lines)
287 marks = bisect(lines, marks, min, max,
288 lambda l, m: self._update_func(l, m),
289 lambda: self._test_func())
290 for i, mark in enumerate(marks):
291 if i == pos:
292 self.assertEqual(BAD, mark)
293 else:
294 self.assertNotEqual(BAD, mark)
296 class TestBisectRequired(unittest.TestCase):
297 TEST = """#include <algorithm>
298 #include <set>
299 #include <map>
300 #include <vector>
302 REQ_LINE = "#include <req>"
304 def setUp(self):
305 global FIND_CONFLICTS
306 FIND_CONFLICTS = False
308 def _update_func(self, lines, marks):
309 self.lines = []
310 for i, mark in enumerate(marks):
311 if mark <= TEST_ON:
312 self.lines.append(lines[i])
313 else:
314 self.lines.append('//' + lines[i])
316 def _test_func(self):
317 """ Command function called by bisect.
318 Returns True on Success, False on failure.
320 # If the required line is not there, fail.
321 found = self.REQ_LINE in self.lines
322 return found
324 def test_success(self):
325 lines = self.TEST.split('\n')
326 (marks, min, max) = get_marks(lines)
327 marks = bisect(lines, marks, min, max,
328 lambda l, m: self._update_func(l, m),
329 lambda: self._test_func())
330 self.assertTrue(GOOD not in marks)
332 def test_required(self):
333 lines = self.TEST.split('\n')
334 for pos in range(len(lines) + 1):
335 lines = self.TEST.split('\n')
336 lines.insert(pos, self.REQ_LINE)
337 (marks, min, max) = get_marks(lines)
339 marks = bisect(lines, marks, min, max,
340 lambda l, m: self._update_func(l, m),
341 lambda: self._test_func())
342 for i, mark in enumerate(marks):
343 if i == pos:
344 self.assertEqual(GOOD, mark)
345 else:
346 self.assertNotEqual(GOOD, mark)
348 unittest.main()
350 # vim: set et sw=4 ts=4 expandtab: