office-gobmx/bin/update_pch_bisect
Leonard Sasse 96ffaadbcd tdf#158803 F821: xrange undefined, should be range in py3; remove some unused variables
changing range(len()) to enumerate as suggested
by Arkadiy Illarionov

Change-Id: I5000204281a5f6ded9c411525e612bf84b552f80
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/165505
Reviewed-by: Ilmari Lauhakangas <ilmari.lauhakangas@libreoffice.org>
Tested-by: Ilmari Lauhakangas <ilmari.lauhakangas@libreoffice.org>
2024-06-25 08:14:53 +02:00

350 lines
10 KiB
Python
Executable file

#! /usr/bin/env python
# -*- Mode: python; tab-width: 4; indent-tabs-mode: t -*-
#
# This file is part of the LibreOffice project.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
"""
This script is to fix precompiled headers.
This script runs in two modes.
In one mode, it starts with a header
that doesn't compile. If finds the
minimum number of includes in the
header to remove to get a successful
run of the command (i.e. compile).
In the second mode, it starts with a
header that compiles fine, however,
it contains one or more required
include without which it wouldn't
compile, which it identifies.
Usage: ./bin/update_pch_bisect ./vcl/inc/pch/precompiled_vcl.hxx "make vcl.build" --find-required --verbose
"""
import sys
import re
import os
import unittest
import subprocess
SILENT = True
FIND_CONFLICTS = True
IGNORE = 0
GOOD = 1
TEST_ON = 2
TEST_OFF = 3
BAD = 4
def run(command):
try:
cmd = command.split(' ', 1)
status = subprocess.call(cmd, stdout=open(os.devnull, 'w'),
stderr=subprocess.STDOUT, close_fds=True)
return True if status == 0 else False
except Exception as e:
sys.stderr.write('Error: {}\n'.format(e))
return False
def update_pch(filename, lines, marks):
with open(filename, 'w') as f:
for i, mark in enumerate(marks):
if mark <= TEST_ON:
f.write(lines[i])
else:
f.write('//' + lines[i])
def log(*args, **kwargs):
global SILENT
if not SILENT:
print(*args, **kwargs)
def bisect(lines, marks, min, max, update, command):
""" Disable half the includes and
calls the command.
Depending on the result,
recurse or return.
"""
global FIND_CONFLICTS
log('Bisecting [{}, {}].'.format(min+1, max))
for i in range(min, max):
if marks[i] != IGNORE:
marks[i] = TEST_ON if FIND_CONFLICTS else TEST_OFF
assume_fail = False
if not FIND_CONFLICTS:
on_list = [x for x in marks if x in (TEST_ON, GOOD)]
assume_fail = (len(on_list) == 0)
update(lines, marks)
if assume_fail or not command():
# Failed
log('Failed [{}, {}].'.format(min+1, max))
if min >= max - 1:
if not FIND_CONFLICTS:
# Try with this one alone.
marks[min] = TEST_ON
update(lines, marks)
if command():
log(' Found @{}: {}'.format(min+1, lines[min].strip('\n')))
marks[min] = GOOD
return marks
else:
log(' Found @{}: {}'.format(min+1, lines[min].strip('\n')))
# Either way, this one is irrelevant.
marks[min] = BAD
return marks
# Bisect
for i in range(min, max):
if marks[i] != IGNORE:
marks[i] = TEST_OFF if FIND_CONFLICTS else TEST_ON
half = min + ((max - min) / 2)
marks = bisect(lines, marks, min, half, update, command)
marks = bisect(lines, marks, half, max, update, command)
else:
# Success
if FIND_CONFLICTS:
log(' Good [{}, {}].'.format(min+1, max))
for i in range(min, max):
if marks[i] != IGNORE:
marks[i] = GOOD
return marks
def get_filename(line):
""" Strips the line from the
'#include' and angled brackets
and return the filename only.
"""
return re.sub(r'(.*#include\s*)<(.*)>(.*)', r'\2', line)
def get_marks(lines):
marks = []
min = -1
max = -1
for i, line in enumerate(lines):
if line.startswith('#include'):
marks.append(TEST_ON)
min = i if min < 0 else min
max = i
else:
marks.append(IGNORE)
return (marks, min, max+1)
def main():
global FIND_CONFLICTS
global SILENT
filename = sys.argv[1]
command = sys.argv[2]
for i in range(3, len(sys.argv)):
opt = sys.argv[i]
if opt == '--find-conflicts':
FIND_CONFLICTS = True
elif opt == '--find-required':
FIND_CONFLICTS = False
elif opt == '--verbose':
SILENT = False
else:
sys.stderr.write('Error: Unknown option [{}].\n'.format(opt))
return 1
lines = []
with open(filename) as f:
lines = f.readlines()
(marks, min, max) = get_marks(lines)
# Test preconditions.
log('Validating all-excluded state...')
for i in range(min, max):
if marks[i] != IGNORE:
marks[i] = TEST_OFF
update_pch(filename, lines, marks)
res = run(command)
if FIND_CONFLICTS:
# Must build all excluded.
if not res:
sys.stderr.write("Error: broken state when all excluded, fix first and try again.")
return 1
else:
# If builds all excluded, we can't bisect.
if res:
sys.stderr.write("Done: in good state when all excluded, nothing to do.")
return 1
# Must build all included.
log('Validating all-included state...')
for i in range(min, max):
if marks[i] != IGNORE:
marks[i] = TEST_ON
update_pch(filename, lines, marks)
if not run(command):
sys.stderr.write("Error: broken state without modifying, fix first and try again.")
return 1
marks = bisect(lines, marks, min, max+1,
lambda l, m: update_pch(filename, l, m),
lambda: run(command))
if not FIND_CONFLICTS:
# Simplify further, as sometimes we can have
# false positives due to the benign nature
# of includes that are not absolutely required.
for i, mark in enumerate(marks):
if mark == GOOD:
marks[i] = TEST_OFF
update_pch(filename, lines, marks)
if not run(command):
# Revert.
marks[i] = GOOD
else:
marks[i] = BAD
elif mark == TEST_OFF:
marks[i] = TEST_ON
update_pch(filename, lines, marks)
log('')
for i, mark in enumerate(marks):
if mark == (BAD if FIND_CONFLICTS else GOOD):
print("'{}',".format(get_filename(lines[i].strip('\n'))))
return 0
if __name__ == '__main__':
if len(sys.argv) in (3, 4, 5):
status = main()
sys.exit(status)
print('Usage: {} <pch> <command> [--find-conflicts]|[--find-required] [--verbose]\n'.format(sys.argv[0]))
print(' --find-conflicts - Finds all conflicting includes. (Default)')
print(' Must compile without any includes.\n')
print(' --find-required - Finds all required includes.')
print(' Must compile with all includes.\n')
print(' --verbose - print noisy progress.')
print('Example: ./bin/update_pch_bisect ./vcl/inc/pch/precompiled_vcl.hxx "make vcl.build" --find-required --verbose')
print('\nRunning unit-tests...')
class TestBisectConflict(unittest.TestCase):
TEST = """ /* Test header. */
#include <memory>
#include <set>
#include <algorithm>
#include <vector>
/* blah blah */
"""
BAD_LINE = "#include <bad>"
def setUp(self):
global FIND_CONFLICTS
FIND_CONFLICTS = True
def _update_func(self, lines, marks):
self.lines = []
for i, mark in enumerate(marks):
if mark <= TEST_ON:
self.lines.append(lines[i])
else:
self.lines.append('//' + lines[i])
def _test_func(self):
""" Command function called by bisect.
Returns True on Success, False on failure.
"""
# If the bad line is still there, fail.
return self.BAD_LINE not in self.lines
def test_success(self):
lines = self.TEST.split('\n')
(marks, min, max) = get_marks(lines)
marks = bisect(lines, marks, min, max,
lambda l, m: self._update_func(l, m),
lambda: self._test_func())
self.assertTrue(BAD not in marks)
def test_conflict(self):
lines = self.TEST.split('\n')
for pos in range(len(lines) + 1):
lines = self.TEST.split('\n')
lines.insert(pos, self.BAD_LINE)
(marks, min, max) = get_marks(lines)
marks = bisect(lines, marks, min, max,
lambda l, m: self._update_func(l, m),
lambda: self._test_func())
for i, mark in enumerate(marks):
if i == pos:
self.assertEqual(BAD, mark)
else:
self.assertNotEqual(BAD, mark)
class TestBisectRequired(unittest.TestCase):
TEST = """#include <algorithm>
#include <set>
#include <map>
#include <vector>
"""
REQ_LINE = "#include <req>"
def setUp(self):
global FIND_CONFLICTS
FIND_CONFLICTS = False
def _update_func(self, lines, marks):
self.lines = []
for i, mark in enumerate(marks):
if mark <= TEST_ON:
self.lines.append(lines[i])
else:
self.lines.append('//' + lines[i])
def _test_func(self):
""" Command function called by bisect.
Returns True on Success, False on failure.
"""
# If the required line is not there, fail.
found = self.REQ_LINE in self.lines
return found
def test_success(self):
lines = self.TEST.split('\n')
(marks, min, max) = get_marks(lines)
marks = bisect(lines, marks, min, max,
lambda l, m: self._update_func(l, m),
lambda: self._test_func())
self.assertTrue(GOOD not in marks)
def test_required(self):
lines = self.TEST.split('\n')
for pos in range(len(lines) + 1):
lines = self.TEST.split('\n')
lines.insert(pos, self.REQ_LINE)
(marks, min, max) = get_marks(lines)
marks = bisect(lines, marks, min, max,
lambda l, m: self._update_func(l, m),
lambda: self._test_func())
for i, mark in enumerate(marks):
if i == pos:
self.assertEqual(GOOD, mark)
else:
self.assertNotEqual(GOOD, mark)
unittest.main()
# vim: set et sw=4 ts=4 expandtab: