| #!/usr/bin/env python3 |
| # |
| # Copyright 2024 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Tests building sqlite and optionally running a sql script. |
| |
| Generates amalgamations, builds the sqlite shell, and optionally runs a sql |
| file in the sqlite shell. Designed to be passed to `git bisect run` to |
| determine a culprit in the sqlite repro for either an issue in the build or a |
| sqlite script. |
| |
| Example usage: |
| git bisect start c12b0d5b7135 9e45bccab2b8 |
| git bisect run ../scripts/repro_tool.py repro.sql --should_build |
| """ |
| |
| import os |
| import argparse |
| import subprocess |
| import sys |
| import textwrap |
| |
| from functools import cache |
| from pathlib import PurePath |
| |
| |
| class SqlTester: |
| |
| def __init__(self, build_dir, quiet, verbose, should_build): |
| self.relative_build_dir = build_dir |
| self.quiet = quiet |
| self.verbose = verbose and not quiet |
| self.should_build = should_build |
| |
| @property |
| @cache |
| def script_dir(self): |
| return os.path.dirname(os.path.realpath(__file__)) |
| |
| @property |
| @cache |
| def chrome_root(self): |
| result = subprocess.run(['git', 'rev-parse', '--show-toplevel'], |
| encoding='UTF-8', |
| cwd=self.script_dir, |
| capture_output=True, |
| text=True) |
| return result.stdout.strip() |
| |
| @property |
| @cache |
| def build_dir(self): |
| return os.path.join(self.chrome_root, self.relative_build_dir) |
| |
| @property |
| @cache |
| def generate_amalgamation_script(self): |
| return os.path.join(self.script_dir, 'generate_amalgamation.py') |
| |
| @property |
| @cache |
| def sqlite_shell(self): |
| return os.path.join(self.build_dir, 'sqlite_shell') |
| |
| def run_process(self, process, stdin=None): |
| stdout = None if self.verbose else open(os.devnull, 'w') |
| process = subprocess.Popen(process, |
| encoding='UTF-8', |
| cwd=self.chrome_root, |
| stdin=stdin, |
| stdout=stdout, |
| stderr=subprocess.PIPE) |
| _, stderr = process.communicate() |
| return process.returncode, stderr |
| |
| def generate_amalgamation(self): |
| return self.run_process(self.generate_amalgamation_script) |
| |
| def build_sqlite_shell(self): |
| return self.run_process( |
| ['autoninja', '-C', self.build_dir, 'sqlite_shell']) |
| |
| def run_sql(self, sqlfile): |
| sqlfileobject = open(sqlfile, 'r', encoding='UTF-8') |
| return self.run_process(self.sqlite_shell, stdin=sqlfileobject) |
| |
| def print(self, message): |
| if not self.quiet: |
| print(message) |
| |
| def print_stderr(self, stderr): |
| self.print(textwrap.indent(stderr, ' | ')) |
| |
| def handle_build_error(self, stderr): |
| if self.should_build: |
| self.print(' Unexpectedly failed:') |
| self.print_stderr(stderr) |
| |
| # -1 indicates to `git bisect run` to abort: |
| # https://git-scm.com/docs/git-bisect#_bisect_run |
| return -1 |
| |
| self.print(' Failed:') |
| self.print_stderr(stderr) |
| return 1 |
| |
| def run(self, sqlfile): |
| if not os.path.isfile(self.generate_amalgamation_script): |
| self.print('generate_amalgamation.py no longer exists in the same ' |
| 'directory as this script or has been renamed.') |
| |
| # -1 indicates to `git bisect run` to abort: |
| # https://git-scm.com/docs/git-bisect#_bisect_run |
| return -1 |
| |
| self.print('Generating amalgamations') |
| returncode, stderr = self.generate_amalgamation() |
| |
| if returncode != 0: |
| return self.handle_build_error(stderr) |
| |
| self.print('Building sqlite_shell') |
| returncode, stderr = self.build_sqlite_shell() |
| |
| if returncode != 0: |
| return self.handle_build_error(stderr) |
| |
| if sqlfile == None: |
| return 0 |
| |
| self.print('Running sqlfile') |
| returncode, stderr = self.run_sql(sqlfile) |
| |
| if returncode != 0: |
| self.print(' Failed:') |
| self.print_stderr(stderr) |
| return 1 |
| |
| self.print(' Success!') |
| return 0 |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter) |
| |
| parser.add_argument( |
| '-C', |
| dest='build_dir', |
| default='out/Default', |
| nargs='?', |
| required=False, |
| help='The Chromium build directory. This is always relative the root ' |
| 'of the Chromium repo.') |
| |
| parser.add_argument( |
| '-q', |
| '--quiet', |
| dest='quiet', |
| required=False, |
| action='store_true', |
| help='Don\'t print to console. Takes precedence over the verbose ' |
| 'flag.') |
| |
| parser.add_argument( |
| '-v', |
| '--verbose', |
| dest='verbose', |
| required=False, |
| action='store_true', |
| help='Print subprocess output. The quiet flag takes precedence.') |
| |
| parser.add_argument( |
| '--should_build', |
| dest='should_build', |
| required=False, |
| action='store_true', |
| help= |
| 'If the build fails, exits with code that indicates `git bisect run` ' |
| 'to abort.') |
| |
| parser.add_argument('sqlfile', |
| nargs='?', |
| help='Path to the sqlite file that repros a crash') |
| |
| args = parser.parse_args() |
| |
| sqlTester = SqlTester(args.build_dir, args.quiet, args.verbose, |
| args.should_build) |
| |
| return sqlTester.run(args.sqlfile) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |