Cosmetics.
[LibreOffice.git] / bin / update_pch_bisect
blob07f0dbb650ae01b8f8b471bffaf1bbc65852e5de
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 from __future__ import print_function
32 import sys
33 import re
34 import os
35 import unittest
36 import subprocess
38 SILENT = True
39 FIND_CONFLICTS = True
41 IGNORE = 0
42 GOOD = 1
43 TEST_ON = 2
44 TEST_OFF = 3
45 BAD = 4
47 def run(command):
48 try:
49 cmd = command.split(' ', 1)
50 status = subprocess.call(cmd, stdout=open(os.devnull, 'w'),
51 stderr=subprocess.STDOUT, close_fds=True)
52 return True if status == 0 else False
53 except Exception as e:
54 sys.stderr.write('Error: {}\n'.format(e))
55 return False
57 def update_pch(filename, lines, marks):
58 with open(filename, 'w') as f:
59 for i in xrange(len(marks)):
60 mark = marks[i]
61 if mark <= TEST_ON:
62 f.write(lines[i])
63 else:
64 f.write('//' + lines[i])
66 def log(*args, **kwargs):
67 global SILENT
68 if not SILENT:
69 print(*args, **kwargs)
71 def bisect(lines, marks, min, max, update, command):
72 """ Disable half the includes and
73 calls the command.
74 Depending on the result,
75 recurse or return.
76 """
77 global FIND_CONFLICTS
79 log('Bisecting [{}, {}].'.format(min+1, max))
80 for i in range(min, max):
81 if marks[i] != IGNORE:
82 marks[i] = TEST_ON if FIND_CONFLICTS else TEST_OFF
84 assume_fail = False
85 if not FIND_CONFLICTS:
86 on_list = [x for x in marks if x in (TEST_ON, GOOD)]
87 assume_fail = (len(on_list) == 0)
89 update(lines, marks)
90 if assume_fail or not command():
91 # Failed
92 log('Failed [{}, {}].'.format(min+1, max))
93 if min >= max - 1:
94 if not FIND_CONFLICTS:
95 # Try with this one alone.
96 marks[min] = TEST_ON
97 update(lines, marks)
98 if command():
99 log(' Found @{}: {}'.format(min+1, lines[min].strip('\n')))
100 marks[min] = GOOD
101 return marks
102 else:
103 log(' Found @{}: {}'.format(min+1, lines[min].strip('\n')))
104 # Either way, this one is irrelevant.
105 marks[min] = BAD
106 return marks
108 # Bisect
109 for i in range(min, max):
110 if marks[i] != IGNORE:
111 marks[i] = TEST_OFF if FIND_CONFLICTS else TEST_ON
113 half = min + ((max - min) / 2)
114 marks = bisect(lines, marks, min, half, update, command)
115 marks = bisect(lines, marks, half, max, update, command)
116 else:
117 # Success
118 if FIND_CONFLICTS:
119 log(' Good [{}, {}].'.format(min+1, max))
120 for i in range(min, max):
121 if marks[i] != IGNORE:
122 marks[i] = GOOD
124 return marks
126 def get_filename(line):
127 """ Strips the line from the
128 '#include' and angled brakets
129 and return the filename only.
131 return re.sub(r'(.*#include\s*)<(.*)>(.*)', r'\2', line)
133 def get_marks(lines):
134 marks = []
135 min = -1
136 max = -1
137 for i in xrange(len(lines)):
138 line = lines[i]
139 if line.startswith('#include'):
140 marks.append(TEST_ON)
141 min = i if min < 0 else min
142 max = i
143 else:
144 marks.append(IGNORE)
146 return (marks, min, max+1)
148 def main():
150 global FIND_CONFLICTS
151 global SILENT
153 filename = sys.argv[1]
154 command = sys.argv[2]
156 for i in range(3, len(sys.argv)):
157 opt = sys.argv[i]
158 if opt == '--find-conflicts':
159 FIND_CONFLICTS = True
160 elif opt == '--find-required':
161 FIND_CONFLICTS = False
162 elif opt == '--verbose':
163 SILENT = False
164 else:
165 sys.stderr.write('Error: Unknown option [{}].\n'.format(opt))
166 return 1
168 lines = []
169 with open(filename) as f:
170 lines = f.readlines()
172 (marks, min, max) = get_marks(lines)
174 # Test preconditions.
175 log('Validating all-excluded state...')
176 for i in range(min, max):
177 if marks[i] != IGNORE:
178 marks[i] = TEST_OFF
179 update_pch(filename, lines, marks)
180 res = run(command)
182 if FIND_CONFLICTS:
183 # Must build all excluded.
184 if not res:
185 sys.stderr.write("Error: broken state when all excluded, fix first and try again.")
186 return 1
187 else:
188 # If builds all excluded, we can't bisect.
189 if res:
190 sys.stderr.write("Done: in good state when all excluded, nothing to do.")
191 return 1
193 # Must build all included.
194 log('Validating all-included state...')
195 for i in range(min, max):
196 if marks[i] != IGNORE:
197 marks[i] = TEST_ON
198 update_pch(filename, lines, marks)
199 if not run(command):
200 sys.stderr.write("Error: broken state without modifying, fix first and try again.")
201 return 1
203 marks = bisect(lines, marks, min, max+1,
204 lambda l, m: update_pch(filename, l, m),
205 lambda: run(command))
206 if not FIND_CONFLICTS:
207 # Simplify further, as sometimes we can have
208 # false positives due to the benign nature
209 # of includes that are not absolutely required.
210 for i in xrange(len(marks)):
211 if marks[i] == GOOD:
212 marks[i] = TEST_OFF
213 update_pch(filename, lines, marks)
214 if not run(command):
215 # Revert.
216 marks[i] = GOOD
217 else:
218 marks[i] = BAD
219 elif marks[i] == TEST_OFF:
220 marks[i] = TEST_ON
222 update_pch(filename, lines, marks)
224 log('')
225 for i in xrange(len(marks)):
226 if marks[i] == (BAD if FIND_CONFLICTS else GOOD):
227 print("'{}',".format(get_filename(lines[i].strip('\n'))))
229 return 0
231 if __name__ == '__main__':
233 if len(sys.argv) in (3, 4, 5):
234 status = main()
235 sys.exit(status)
237 print('Usage: {} <pch> <command> [--find-conflicts]|[--find-required] [--verbose]\n'.format(sys.argv[0]))
238 print(' --find-conflicts - Finds all conflicting includes. (Default)')
239 print(' Must compile without any includes.\n')
240 print(' --find-required - Finds all required includes.')
241 print(' Must compile with all includes.\n')
242 print(' --verbose - print noisy progress.')
243 print('Example: ./bin/update_pch_bisect ./vcl/inc/pch/precompiled_vcl.hxx "make vcl.build" --find-required --verbose')
244 print('\nRunning unit-tests...')
247 class TestBisectConflict(unittest.TestCase):
248 TEST = """ /* Test header. */
249 #include <memory>
250 #include <set>
251 #include <algorithm>
252 #include <vector>
253 /* blah blah */
255 BAD_LINE = "#include <bad>"
257 def setUp(self):
258 global FIND_CONFLICTS
259 FIND_CONFLICTS = True
261 def _update_func(self, lines, marks):
262 self.lines = []
263 for i in xrange(len(marks)):
264 mark = marks[i]
265 if mark <= TEST_ON:
266 self.lines.append(lines[i])
267 else:
268 self.lines.append('//' + lines[i])
270 def _test_func(self):
271 """ Command function called by bisect.
272 Returns True on Success, False on failure.
274 # If the bad line is still there, fail.
275 return self.BAD_LINE not in self.lines
277 def test_success(self):
278 lines = self.TEST.split('\n')
279 (marks, min, max) = get_marks(lines)
280 marks = bisect(lines, marks, min, max,
281 lambda l, m: self._update_func(l, m),
282 lambda: self._test_func())
283 self.assertTrue(BAD not in marks)
285 def test_conflict(self):
286 lines = self.TEST.split('\n')
287 for pos in xrange(len(lines) + 1):
288 lines = self.TEST.split('\n')
289 lines.insert(pos, self.BAD_LINE)
290 (marks, min, max) = get_marks(lines)
292 marks = bisect(lines, marks, min, max,
293 lambda l, m: self._update_func(l, m),
294 lambda: self._test_func())
295 for i in xrange(len(marks)):
296 if i == pos:
297 self.assertEqual(BAD, marks[i])
298 else:
299 self.assertNotEqual(BAD, marks[i])
301 class TestBisectRequired(unittest.TestCase):
302 TEST = """#include <algorithm>
303 #include <set>
304 #include <map>
305 #include <vector>
307 REQ_LINE = "#include <req>"
309 def setUp(self):
310 global FIND_CONFLICTS
311 FIND_CONFLICTS = False
313 def _update_func(self, lines, marks):
314 self.lines = []
315 for i in xrange(len(marks)):
316 mark = marks[i]
317 if mark <= TEST_ON:
318 self.lines.append(lines[i])
319 else:
320 self.lines.append('//' + lines[i])
322 def _test_func(self):
323 """ Command function called by bisect.
324 Returns True on Success, False on failure.
326 # If the required line is not there, fail.
327 found = self.REQ_LINE in self.lines
328 return found
330 def test_success(self):
331 lines = self.TEST.split('\n')
332 (marks, min, max) = get_marks(lines)
333 marks = bisect(lines, marks, min, max,
334 lambda l, m: self._update_func(l, m),
335 lambda: self._test_func())
336 self.assertTrue(GOOD not in marks)
338 def test_required(self):
339 lines = self.TEST.split('\n')
340 for pos in xrange(len(lines) + 1):
341 lines = self.TEST.split('\n')
342 lines.insert(pos, self.REQ_LINE)
343 (marks, min, max) = get_marks(lines)
345 marks = bisect(lines, marks, min, max,
346 lambda l, m: self._update_func(l, m),
347 lambda: self._test_func())
348 for i in xrange(len(marks)):
349 if i == pos:
350 self.assertEqual(GOOD, marks[i])
351 else:
352 self.assertNotEqual(GOOD, marks[i])
354 unittest.main()
356 # vim: set et sw=4 ts=4 expandtab: