From 795dd9e87e44d1c49f160cd003cdde4113ee8247 Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Tue, 13 Apr 2021 17:32:05 +0300 Subject: tools: drop the .py extension --- .github/workflows/basic.yml | 2 +- README.md | 8 +-- docs/clang-format.md | 5 -- docs/ctest-driver.md | 6 +- docs/project-clang-format.md | 5 ++ setup.cfg | 4 +- tools/clang-format.py | 168 ------------------------------------------- tools/ctest-driver | 166 ++++++++++++++++++++++++++++++++++++++++++ tools/ctest-driver.py | 166 ------------------------------------------ tools/project-clang-format | 168 +++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 349 insertions(+), 349 deletions(-) delete mode 100644 docs/clang-format.md create mode 100644 docs/project-clang-format.md delete mode 100755 tools/clang-format.py create mode 100755 tools/ctest-driver delete mode 100755 tools/ctest-driver.py create mode 100755 tools/project-clang-format diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index c2a1d22..197306c 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -16,7 +16,7 @@ jobs: - name: Checkout uses: actions/checkout@v2 - name: Run clang-format - run: ./tools/clang-format.py --clang-format clang-format-10 + run: ./tools/project-clang-format --clang-format clang-format-10 basic: strategy: diff --git a/README.md b/README.md index ea45118..b909353 100644 --- a/README.md +++ b/README.md @@ -145,12 +145,12 @@ done Tools ----- -* [clang-format.py] — `clang-format` all C/C++ files in the project. -* [ctest-driver.py] — wrap an executable for testing with CTest; +* [project-clang-format] — `clang-format` all C/C++ files in the project. +* [ctest-driver] — wrap an executable for testing with CTest; cross-platform `grep`. -[clang-format.py]: docs/clang-format.md -[ctest-driver.py]: docs/ctest-driver.md +[project-clang-format]: docs/project-clang-format.md +[ctest-driver]: docs/ctest-driver.md Examples -------- diff --git a/docs/clang-format.md b/docs/clang-format.md deleted file mode 100644 index 217b2da..0000000 --- a/docs/clang-format.md +++ /dev/null @@ -1,5 +0,0 @@ -`clang-format` all C/C++ files in the project. - - $ cd project/ - $ python3 path/to/tools/clang-format.py # Prints a diff - $ python3 path/to/tools/clang-format.py -i # Edits files in-place diff --git a/docs/ctest-driver.md b/docs/ctest-driver.md index af7c3b2..1a70f5d 100644 --- a/docs/ctest-driver.md +++ b/docs/ctest-driver.md @@ -4,10 +4,10 @@ PASS_REGULAR_EXPRESSION feature: 1. The regular expression syntax used by CMake is deficient. 2. The exit code of a test is ignored if one of the regexes matches. -`ctest-driver.py` tries to fix them. +`ctest-driver` tries to fix them. - $ python3 path/to/tools/ctest-driver.py run --pass-regex OK --fail-regex Fail -- path/to/executable arg1 arg2 + $ python3 path/to/tools/ctest-driver run --pass-regex OK --fail-regex Fail -- path/to/executable arg1 arg2 In addition, it's a cross-platform `grep`: - $ python3 path/to/tools/ctest-driver.py grep --pass-regex OK --fail-regex Fail -- path/to/logfile.log + $ python3 path/to/tools/ctest-driver grep --pass-regex OK --fail-regex Fail -- path/to/logfile.log diff --git a/docs/project-clang-format.md b/docs/project-clang-format.md new file mode 100644 index 0000000..d294730 --- /dev/null +++ b/docs/project-clang-format.md @@ -0,0 +1,5 @@ +`clang-format` all C/C++ files in the project. + + $ cd project/ + $ python3 path/to/tools/project-clang-format # Prints a diff + $ python3 path/to/tools/project-clang-format -i # Edits files in-place diff --git a/setup.cfg b/setup.cfg index b9c42a7..29fad59 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,8 +20,8 @@ classifiers = [options] packages = find: scripts = - tools/clang-format.py - tools/ctest-driver.py + tools/project-clang-format + tools/ctest-driver #python_requires = >=3.6 [options.entry_points] diff --git a/tools/clang-format.py b/tools/clang-format.py deleted file mode 100755 index 81b2f93..0000000 --- a/tools/clang-format.py +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (c) 2020 Egor Tensin -# This file is part of the "cmake-common" project. -# For details, see https://github.com/egor-tensin/cmake-common. -# Distributed under the MIT License. - -'''clang-format all C/C++ files in the project - -This script feeds every C/C++ file in the repository to clang-format, either -printing a unified diff between the original and the formatted versions, or -formatting the files in-place. -''' - -import argparse -from contextlib import contextmanager -import difflib -import logging -import os -import subprocess -import sys - - -@contextmanager -def setup_logging(): - logging.basicConfig( - format='%(asctime)s | %(levelname)s | %(message)s', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.INFO) - try: - yield - except Exception as e: - logging.exception(e) - raise - - -@contextmanager -def cd(path): - cwd = os.getcwd() - os.chdir(path) - try: - yield - finally: - os.chdir(cwd) - - -def run(cmd_line): - logging.debug('Running executable: %s', cmd_line) - try: - return subprocess.run(cmd_line, check=True, universal_newlines=True, - stderr=subprocess.STDOUT, stdout=subprocess.PIPE) - except subprocess.CalledProcessError as e: - logging.error('Process finished with exit code %d: %s', e.returncode, cmd_line) - logging.error('Its output was:\n%s', e.output) - raise - - -def read_file(path): - with open(path) as file: - return file.read() - - -class ClangFormat: - def __init__(self, path, style): - self.path = path - self.style = style - - def _get_command_line(self, paths, in_place=False): - cmd_line = [self.path, f'-style={self.style}'] - if in_place: - cmd_line.append('-i') - cmd_line.append('--') - cmd_line += list(paths) - return cmd_line - - def format_in_place(self, paths): - run(self._get_command_line(paths, in_place=True)) - - @staticmethod - def _show_diff(path, formatted): - original = read_file(path).split('\n') - formatted = formatted.split('\n') - original_lbl = f'{path} (original)' - formatted_lbl = f'{path} (formatted)' - - diff = difflib.unified_diff(original, formatted, fromfile=original_lbl, - tofile=formatted_lbl, lineterm='') - - clean = True - for line in diff: - clean = False - print(line) - return clean - - def show_diff(self, paths): - clean = True - for path in paths: - formatted = run(self._get_command_line([path])).stdout - clean = self._show_diff(path, formatted) and clean - return clean - - -def git_root_dir(): - cmd_line = ['git', 'rev-parse', '--show-toplevel'] - root_dir = run(cmd_line).stdout - if root_dir[-1] != '\n': - raise RuntimeError('git rev-parse --show-toplevel should append a newline?') - return root_dir[:-1] - - -def list_all_files(): - cmd_line = ['git', 'ls-tree', '-r', '-z', '--name-only', 'HEAD'] - repo_files = run(cmd_line).stdout - repo_files = repo_files.split('\0') - return repo_files - - -CPP_FILE_EXTENSIONS = {'.c', '.h', '.cc', '.hh', '.cpp', '.hpp', '.cxx', '.hxx', '.cp', '.c++'} - - -def list_cpp_files(): - for path in list_all_files(): - ext = os.path.splitext(path)[1] - if ext in CPP_FILE_EXTENSIONS: - yield path - - -def process_cpp_files(args): - clang_format = ClangFormat(args.clang_format, args.style) - with cd(git_root_dir()): - cpp_files = list_cpp_files() - if args.in_place: - clang_format.format_in_place(cpp_files) - else: - if not clang_format.show_diff(cpp_files): - sys.exit(1) - - -DEFAULT_CLANG_FORMAT = 'clang-format' -DEFAULT_STYLE = 'file' - - -def parse_args(argv=None): - if argv is None: - argv = sys.argv[1:] - - parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - - parser.add_argument('-b', '--clang-format', default=DEFAULT_CLANG_FORMAT, - help='clang-format executable file path') - parser.add_argument('-s', '--style', default=DEFAULT_STYLE, - help='clang-format -style parameter argument') - parser.add_argument('-i', '--in-place', action='store_true', - help='edit the files in-place') - - return parser.parse_args(argv) - - -def main(argv=None): - with setup_logging(): - args = parse_args(argv) - process_cpp_files(args) - - -if __name__ == '__main__': - main() diff --git a/tools/ctest-driver b/tools/ctest-driver new file mode 100755 index 0000000..414e20c --- /dev/null +++ b/tools/ctest-driver @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2021 Egor Tensin +# This file is part of the "cmake-common" project. +# For details, see https://github.com/egor-tensin/cmake-common. +# Distributed under the MIT License. + +'''Wrap your actual test driver to use with CTest + +CTest suffers from at least two issues, in particular with regard to its +PASS_REGULAR_EXPRESSION feature: + +1. The regular expression syntax used by CMake is deficient. +2. The exit code of a test is ignored if one of the regexes matches. + +This script tries to fix them. +''' + +import argparse +import os +import re +import subprocess +import sys + + +SCRIPT_NAME = os.path.basename(__file__) + + +def dump(msg, **kwargs): + print(f'{SCRIPT_NAME}: {msg}', **kwargs) + + +def err(msg): + dump(msg, file=sys.stderr) + + +def read_file(path): + with open(path, mode='r') as fd: + return fd.read() + + +def run(cmd_line): + try: + result = subprocess.run(cmd_line, check=True, universal_newlines=True, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE) + assert result.returncode == 0 + return result.stdout + except subprocess.CalledProcessError as e: + sys.stdout.write(e.output) + sys.exit(e.returncode) + + +def run_new_window(cmd_line): + try: + result = subprocess.run(cmd_line, check=True, + creationflags=subprocess.CREATE_NEW_CONSOLE) + assert result.returncode == 0 + return None + except subprocess.CalledProcessError as e: + sys.exit(e.returncode) + + +def match(s, regex): + return re.search(regex, s, flags=re.MULTILINE) + + +def match_any(s, regexes): + return any([match(s, regex) for regex in regexes]) + + +def match_all(s, regexes): + return all([match(s, regex) for regex in regexes]) + + +def match_pass_regexes(output, regexes): + if not regexes: + return + if not match_all(output, regexes): + err("Couldn't match test program's output against all of the"\ + " regular expressions:") + for regex in regexes: + err(f'\t{regex}') + sys.exit(1) + + +def match_fail_regexes(output, regexes): + if not regexes: + return + if match_any(output, regexes): + err("Matched test program's output against some of the regular"\ + " expressions:") + for regex in regexes: + err(f'\t{regex}') + sys.exit(1) + + +def run_actual_test_driver(args): + cmd_line = [args.exe_path] + args.exe_args + run_func = run + if args.new_window: + run_func = run_new_window + output = run_func(cmd_line) + if args.new_window and (args.pass_regexes or args.fail_regexes): + err("Cannot launch child process in a new window and capture its output") + if output is not None: + sys.stdout.write(output) + match_pass_regexes(output, args.pass_regexes) + match_fail_regexes(output, args.fail_regexes) + + +def grep_file(args): + contents = read_file(args.path) + match_pass_regexes(contents, args.pass_regexes) + match_fail_regexes(contents, args.fail_regexes) + + +def parse_args(argv=None): + if argv is None: + argv = sys.argv[1:] + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + + subparsers = parser.add_subparsers(dest='command') + + parser_run = subparsers.add_parser('run', help='run an executable and check its output') + parser_run.add_argument('-p', '--pass-regex', nargs='*', + dest='pass_regexes', metavar='REGEX', + help='pass if all of these regexes match') + parser_run.add_argument('-f', '--fail-regex', nargs='*', + dest='fail_regexes', metavar='REGEX', + help='fail if any of these regexes matches') + parser_run.add_argument('-n', '--new-window', action='store_true', + help='launch child process in a new console window') + parser_run.add_argument('exe_path', metavar='PATH', + help='path to the test executable') + # nargs='*' here would discard additional '--'s. + parser_run.add_argument('exe_args', metavar='ARG', nargs=argparse.REMAINDER, + help='test executable arguments') + parser_run.set_defaults(func=run_actual_test_driver) + + parser_grep = subparsers.add_parser('grep', help='check file contents for matching patterns') + parser_grep.add_argument('-p', '--pass-regex', nargs='*', + dest='pass_regexes', metavar='REGEX', + help='pass if all of these regexes match') + parser_grep.add_argument('-f', '--fail-regex', nargs='*', + dest='fail_regexes', metavar='REGEX', + help='fail if any of these regexes matches') + parser_grep.add_argument('path', metavar='PATH', help='text file path') + parser_grep.set_defaults(func=grep_file) + + args = parser.parse_args(argv) + if args.command is None: + parser.error('please specify a subcommand to run') + return args + + +def main(argv=None): + args = parse_args(argv) + args.func(args) + + +if __name__ == '__main__': + main() diff --git a/tools/ctest-driver.py b/tools/ctest-driver.py deleted file mode 100755 index 414e20c..0000000 --- a/tools/ctest-driver.py +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (c) 2021 Egor Tensin -# This file is part of the "cmake-common" project. -# For details, see https://github.com/egor-tensin/cmake-common. -# Distributed under the MIT License. - -'''Wrap your actual test driver to use with CTest - -CTest suffers from at least two issues, in particular with regard to its -PASS_REGULAR_EXPRESSION feature: - -1. The regular expression syntax used by CMake is deficient. -2. The exit code of a test is ignored if one of the regexes matches. - -This script tries to fix them. -''' - -import argparse -import os -import re -import subprocess -import sys - - -SCRIPT_NAME = os.path.basename(__file__) - - -def dump(msg, **kwargs): - print(f'{SCRIPT_NAME}: {msg}', **kwargs) - - -def err(msg): - dump(msg, file=sys.stderr) - - -def read_file(path): - with open(path, mode='r') as fd: - return fd.read() - - -def run(cmd_line): - try: - result = subprocess.run(cmd_line, check=True, universal_newlines=True, - stderr=subprocess.STDOUT, - stdout=subprocess.PIPE) - assert result.returncode == 0 - return result.stdout - except subprocess.CalledProcessError as e: - sys.stdout.write(e.output) - sys.exit(e.returncode) - - -def run_new_window(cmd_line): - try: - result = subprocess.run(cmd_line, check=True, - creationflags=subprocess.CREATE_NEW_CONSOLE) - assert result.returncode == 0 - return None - except subprocess.CalledProcessError as e: - sys.exit(e.returncode) - - -def match(s, regex): - return re.search(regex, s, flags=re.MULTILINE) - - -def match_any(s, regexes): - return any([match(s, regex) for regex in regexes]) - - -def match_all(s, regexes): - return all([match(s, regex) for regex in regexes]) - - -def match_pass_regexes(output, regexes): - if not regexes: - return - if not match_all(output, regexes): - err("Couldn't match test program's output against all of the"\ - " regular expressions:") - for regex in regexes: - err(f'\t{regex}') - sys.exit(1) - - -def match_fail_regexes(output, regexes): - if not regexes: - return - if match_any(output, regexes): - err("Matched test program's output against some of the regular"\ - " expressions:") - for regex in regexes: - err(f'\t{regex}') - sys.exit(1) - - -def run_actual_test_driver(args): - cmd_line = [args.exe_path] + args.exe_args - run_func = run - if args.new_window: - run_func = run_new_window - output = run_func(cmd_line) - if args.new_window and (args.pass_regexes or args.fail_regexes): - err("Cannot launch child process in a new window and capture its output") - if output is not None: - sys.stdout.write(output) - match_pass_regexes(output, args.pass_regexes) - match_fail_regexes(output, args.fail_regexes) - - -def grep_file(args): - contents = read_file(args.path) - match_pass_regexes(contents, args.pass_regexes) - match_fail_regexes(contents, args.fail_regexes) - - -def parse_args(argv=None): - if argv is None: - argv = sys.argv[1:] - - parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - - subparsers = parser.add_subparsers(dest='command') - - parser_run = subparsers.add_parser('run', help='run an executable and check its output') - parser_run.add_argument('-p', '--pass-regex', nargs='*', - dest='pass_regexes', metavar='REGEX', - help='pass if all of these regexes match') - parser_run.add_argument('-f', '--fail-regex', nargs='*', - dest='fail_regexes', metavar='REGEX', - help='fail if any of these regexes matches') - parser_run.add_argument('-n', '--new-window', action='store_true', - help='launch child process in a new console window') - parser_run.add_argument('exe_path', metavar='PATH', - help='path to the test executable') - # nargs='*' here would discard additional '--'s. - parser_run.add_argument('exe_args', metavar='ARG', nargs=argparse.REMAINDER, - help='test executable arguments') - parser_run.set_defaults(func=run_actual_test_driver) - - parser_grep = subparsers.add_parser('grep', help='check file contents for matching patterns') - parser_grep.add_argument('-p', '--pass-regex', nargs='*', - dest='pass_regexes', metavar='REGEX', - help='pass if all of these regexes match') - parser_grep.add_argument('-f', '--fail-regex', nargs='*', - dest='fail_regexes', metavar='REGEX', - help='fail if any of these regexes matches') - parser_grep.add_argument('path', metavar='PATH', help='text file path') - parser_grep.set_defaults(func=grep_file) - - args = parser.parse_args(argv) - if args.command is None: - parser.error('please specify a subcommand to run') - return args - - -def main(argv=None): - args = parse_args(argv) - args.func(args) - - -if __name__ == '__main__': - main() diff --git a/tools/project-clang-format b/tools/project-clang-format new file mode 100755 index 0000000..81b2f93 --- /dev/null +++ b/tools/project-clang-format @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2020 Egor Tensin +# This file is part of the "cmake-common" project. +# For details, see https://github.com/egor-tensin/cmake-common. +# Distributed under the MIT License. + +'''clang-format all C/C++ files in the project + +This script feeds every C/C++ file in the repository to clang-format, either +printing a unified diff between the original and the formatted versions, or +formatting the files in-place. +''' + +import argparse +from contextlib import contextmanager +import difflib +import logging +import os +import subprocess +import sys + + +@contextmanager +def setup_logging(): + logging.basicConfig( + format='%(asctime)s | %(levelname)s | %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + level=logging.INFO) + try: + yield + except Exception as e: + logging.exception(e) + raise + + +@contextmanager +def cd(path): + cwd = os.getcwd() + os.chdir(path) + try: + yield + finally: + os.chdir(cwd) + + +def run(cmd_line): + logging.debug('Running executable: %s', cmd_line) + try: + return subprocess.run(cmd_line, check=True, universal_newlines=True, + stderr=subprocess.STDOUT, stdout=subprocess.PIPE) + except subprocess.CalledProcessError as e: + logging.error('Process finished with exit code %d: %s', e.returncode, cmd_line) + logging.error('Its output was:\n%s', e.output) + raise + + +def read_file(path): + with open(path) as file: + return file.read() + + +class ClangFormat: + def __init__(self, path, style): + self.path = path + self.style = style + + def _get_command_line(self, paths, in_place=False): + cmd_line = [self.path, f'-style={self.style}'] + if in_place: + cmd_line.append('-i') + cmd_line.append('--') + cmd_line += list(paths) + return cmd_line + + def format_in_place(self, paths): + run(self._get_command_line(paths, in_place=True)) + + @staticmethod + def _show_diff(path, formatted): + original = read_file(path).split('\n') + formatted = formatted.split('\n') + original_lbl = f'{path} (original)' + formatted_lbl = f'{path} (formatted)' + + diff = difflib.unified_diff(original, formatted, fromfile=original_lbl, + tofile=formatted_lbl, lineterm='') + + clean = True + for line in diff: + clean = False + print(line) + return clean + + def show_diff(self, paths): + clean = True + for path in paths: + formatted = run(self._get_command_line([path])).stdout + clean = self._show_diff(path, formatted) and clean + return clean + + +def git_root_dir(): + cmd_line = ['git', 'rev-parse', '--show-toplevel'] + root_dir = run(cmd_line).stdout + if root_dir[-1] != '\n': + raise RuntimeError('git rev-parse --show-toplevel should append a newline?') + return root_dir[:-1] + + +def list_all_files(): + cmd_line = ['git', 'ls-tree', '-r', '-z', '--name-only', 'HEAD'] + repo_files = run(cmd_line).stdout + repo_files = repo_files.split('\0') + return repo_files + + +CPP_FILE_EXTENSIONS = {'.c', '.h', '.cc', '.hh', '.cpp', '.hpp', '.cxx', '.hxx', '.cp', '.c++'} + + +def list_cpp_files(): + for path in list_all_files(): + ext = os.path.splitext(path)[1] + if ext in CPP_FILE_EXTENSIONS: + yield path + + +def process_cpp_files(args): + clang_format = ClangFormat(args.clang_format, args.style) + with cd(git_root_dir()): + cpp_files = list_cpp_files() + if args.in_place: + clang_format.format_in_place(cpp_files) + else: + if not clang_format.show_diff(cpp_files): + sys.exit(1) + + +DEFAULT_CLANG_FORMAT = 'clang-format' +DEFAULT_STYLE = 'file' + + +def parse_args(argv=None): + if argv is None: + argv = sys.argv[1:] + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument('-b', '--clang-format', default=DEFAULT_CLANG_FORMAT, + help='clang-format executable file path') + parser.add_argument('-s', '--style', default=DEFAULT_STYLE, + help='clang-format -style parameter argument') + parser.add_argument('-i', '--in-place', action='store_true', + help='edit the files in-place') + + return parser.parse_args(argv) + + +def main(argv=None): + with setup_logging(): + args = parse_args(argv) + process_cpp_files(args) + + +if __name__ == '__main__': + main() -- cgit v1.2.3