Bump version to 24.04.3.4
[LibreOffice.git] / bin / update_pch_bisect
blob271cbc88ff1b500311962590bdbbe670781374d6
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 in xrange(len(marks)):
58 mark = marks[i]
59 if mark <= TEST_ON:
60 f.write(lines[i])
61 else:
62 f.write('//' + lines[i])
64 def log(*args, **kwargs):
65 global SILENT
66 if not SILENT:
67 print(*args, **kwargs)
69 def bisect(lines, marks, min, max, update, command):
70 """ Disable half the includes and
71 calls the command.
72 Depending on the result,
73 recurse or return.
74 """
75 global FIND_CONFLICTS
77 log('Bisecting [{}, {}].'.format(min+1, max))
78 for i in range(min, max):
79 if marks[i] != IGNORE:
80 marks[i] = TEST_ON if FIND_CONFLICTS else TEST_OFF
82 assume_fail = False
83 if not FIND_CONFLICTS:
84 on_list = [x for x in marks if x in (TEST_ON, GOOD)]
85 assume_fail = (len(on_list) == 0)
87 update(lines, marks)
88 if assume_fail or not command():
89 # Failed
90 log('Failed [{}, {}].'.format(min+1, max))
91 if min >= max - 1:
92 if not FIND_CONFLICTS:
93 # Try with this one alone.
94 marks[min] = TEST_ON
95 update(lines, marks)
96 if command():
97 log(' Found @{}: {}'.format(min+1, lines[min].strip('\n')))
98 marks[min] = GOOD
99 return marks
100 else:
101 log(' Found @{}: {}'.format(min+1, lines[min].strip('\n')))
102 # Either way, this one is irrelevant.
103 marks[min] = BAD
104 return marks
106 # Bisect
107 for i in range(min, max):
108 if marks[i] != IGNORE:
109 marks[i] = TEST_OFF if FIND_CONFLICTS else TEST_ON
111 half = min + ((max - min) / 2)
112 marks = bisect(lines, marks, min, half, update, command)
113 marks = bisect(lines, marks, half, max, update, command)
114 else:
115 # Success
116 if FIND_CONFLICTS:
117 log(' Good [{}, {}].'.format(min+1, max))
118 for i in range(min, max):
119 if marks[i] != IGNORE:
120 marks[i] = GOOD
122 return marks
124 def get_filename(line):
125 """ Strips the line from the
126 '#include' and angled brackets
127 and return the filename only.
129 return re.sub(r'(.*#include\s*)<(.*)>(.*)', r'\2', line)
131 def get_marks(lines):
132 marks = []
133 min = -1
134 max = -1
135 for i in xrange(len(lines)):
136 line = lines[i]
137 if line.startswith('#include'):
138 marks.append(TEST_ON)
139 min = i if min < 0 else min
140 max = i
141 else:
142 marks.append(IGNORE)
144 return (marks, min, max+1)
146 def main():
148 global FIND_CONFLICTS
149 global SILENT
151 filename = sys.argv[1]
152 command = sys.argv[2]
154 for i in range(3, len(sys.argv)):
155 opt = sys.argv[i]
156 if opt == '--find-conflicts':
157 FIND_CONFLICTS = True
158 elif opt == '--find-required':
159 FIND_CONFLICTS = False
160 elif opt == '--verbose':
161 SILENT = False
162 else:
163 sys.stderr.write('Error: Unknown option [{}].\n'.format(opt))
164 return 1
166 lines = []
167 with open(filename) as f:
168 lines = f.readlines()
170 (marks, min, max) = get_marks(lines)
172 # Test preconditions.
173 log('Validating all-excluded state...')
174 for i in range(min, max):
175 if marks[i] != IGNORE:
176 marks[i] = TEST_OFF
177 update_pch(filename, lines, marks)
178 res = run(command)
180 if FIND_CONFLICTS:
181 # Must build all excluded.
182 if not res:
183 sys.stderr.write("Error: broken state when all excluded, fix first and try again.")
184 return 1
185 else:
186 # If builds all excluded, we can't bisect.
187 if res:
188 sys.stderr.write("Done: in good state when all excluded, nothing to do.")
189 return 1
191 # Must build all included.
192 log('Validating all-included state...')
193 for i in range(min, max):
194 if marks[i] != IGNORE:
195 marks[i] = TEST_ON
196 update_pch(filename, lines, marks)
197 if not run(command):
198 sys.stderr.write("Error: broken state without modifying, fix first and try again.")
199 return 1
201 marks = bisect(lines, marks, min, max+1,
202 lambda l, m: update_pch(filename, l, m),
203 lambda: run(command))
204 if not FIND_CONFLICTS:
205 # Simplify further, as sometimes we can have
206 # false positives due to the benign nature
207 # of includes that are not absolutely required.
208 for i in xrange(len(marks)):
209 if marks[i] == GOOD:
210 marks[i] = TEST_OFF
211 update_pch(filename, lines, marks)
212 if not run(command):
213 # Revert.
214 marks[i] = GOOD
215 else:
216 marks[i] = BAD
217 elif marks[i] == TEST_OFF:
218 marks[i] = TEST_ON
220 update_pch(filename, lines, marks)
222 log('')
223 for i in xrange(len(marks)):
224 if marks[i] == (BAD if FIND_CONFLICTS else GOOD):
225 print("'{}',".format(get_filename(lines[i].strip('\n'))))
227 return 0
229 if __name__ == '__main__':
231 if len(sys.argv) in (3, 4, 5):
232 status = main()
233 sys.exit(status)
235 print('Usage: {} <pch> <command> [--find-conflicts]|[--find-required] [--verbose]\n'.format(sys.argv[0]))
236 print(' --find-conflicts - Finds all conflicting includes. (Default)')
237 print(' Must compile without any includes.\n')
238 print(' --find-required - Finds all required includes.')
239 print(' Must compile with all includes.\n')
240 print(' --verbose - print noisy progress.')
241 print('Example: ./bin/update_pch_bisect ./vcl/inc/pch/precompiled_vcl.hxx "make vcl.build" --find-required --verbose')
242 print('\nRunning unit-tests...')
245 class TestBisectConflict(unittest.TestCase):
246 TEST = """ /* Test header. */
247 #include <memory>
248 #include <set>
249 #include <algorithm>
250 #include <vector>
251 /* blah blah */
253 BAD_LINE = "#include <bad>"
255 def setUp(self):
256 global FIND_CONFLICTS
257 FIND_CONFLICTS = True
259 def _update_func(self, lines, marks):
260 self.lines = []
261 for i in xrange(len(marks)):
262 mark = marks[i]
263 if mark <= TEST_ON:
264 self.lines.append(lines[i])
265 else:
266 self.lines.append('//' + lines[i])
268 def _test_func(self):
269 """ Command function called by bisect.
270 Returns True on Success, False on failure.
272 # If the bad line is still there, fail.
273 return self.BAD_LINE not in self.lines
275 def test_success(self):
276 lines = self.TEST.split('\n')
277 (marks, min, max) = get_marks(lines)
278 marks = bisect(lines, marks, min, max,
279 lambda l, m: self._update_func(l, m),
280 lambda: self._test_func())
281 self.assertTrue(BAD not in marks)
283 def test_conflict(self):
284 lines = self.TEST.split('\n')
285 for pos in xrange(len(lines) + 1):
286 lines = self.TEST.split('\n')
287 lines.insert(pos, self.BAD_LINE)
288 (marks, min, max) = get_marks(lines)
290 marks = bisect(lines, marks, min, max,
291 lambda l, m: self._update_func(l, m),
292 lambda: self._test_func())
293 for i in xrange(len(marks)):
294 if i == pos:
295 self.assertEqual(BAD, marks[i])
296 else:
297 self.assertNotEqual(BAD, marks[i])
299 class TestBisectRequired(unittest.TestCase):
300 TEST = """#include <algorithm>
301 #include <set>
302 #include <map>
303 #include <vector>
305 REQ_LINE = "#include <req>"
307 def setUp(self):
308 global FIND_CONFLICTS
309 FIND_CONFLICTS = False
311 def _update_func(self, lines, marks):
312 self.lines = []
313 for i in xrange(len(marks)):
314 mark = marks[i]
315 if mark <= TEST_ON:
316 self.lines.append(lines[i])
317 else:
318 self.lines.append('//' + lines[i])
320 def _test_func(self):
321 """ Command function called by bisect.
322 Returns True on Success, False on failure.
324 # If the required line is not there, fail.
325 found = self.REQ_LINE in self.lines
326 return found
328 def test_success(self):
329 lines = self.TEST.split('\n')
330 (marks, min, max) = get_marks(lines)
331 marks = bisect(lines, marks, min, max,
332 lambda l, m: self._update_func(l, m),
333 lambda: self._test_func())
334 self.assertTrue(GOOD not in marks)
336 def test_required(self):
337 lines = self.TEST.split('\n')
338 for pos in xrange(len(lines) + 1):
339 lines = self.TEST.split('\n')
340 lines.insert(pos, self.REQ_LINE)
341 (marks, min, max) = get_marks(lines)
343 marks = bisect(lines, marks, min, max,
344 lambda l, m: self._update_func(l, m),
345 lambda: self._test_func())
346 for i in xrange(len(marks)):
347 if i == pos:
348 self.assertEqual(GOOD, marks[i])
349 else:
350 self.assertNotEqual(GOOD, marks[i])
352 unittest.main()
354 # vim: set et sw=4 ts=4 expandtab: