diff options
Diffstat (limited to 'cts/cts-regression.in')
-rw-r--r-- | cts/cts-regression.in | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/cts/cts-regression.in b/cts/cts-regression.in new file mode 100644 index 0000000..c6837c4 --- /dev/null +++ b/cts/cts-regression.in @@ -0,0 +1,293 @@ +#!@PYTHON@ +"""Convenience wrapper for running Pacemaker regression tests. + +Usage: cts-regression [-h] [-V] [-v] [COMPONENT ...] +""" + +__copyright__ = 'Copyright 2012-2023 the Pacemaker project contributors' +__license__ = 'GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY' + +import argparse +import os +import subprocess +import sys +import textwrap + +# These imports allow running from a source checkout after running `make`. +# Note that while this doesn't necessarily mean it will successfully run tests, +# but being able to see --help output can be useful. +if os.path.exists("@abs_top_srcdir@/python"): + sys.path.insert(0, "@abs_top_srcdir@/python") + +if os.path.exists("@abs_top_builddir@/python") and "@abs_top_builddir@" != "@abs_top_srcdir@": + sys.path.insert(0, "@abs_top_builddir@/python") + +from pacemaker.buildoptions import BuildOptions +from pacemaker.exitstatus import ExitStatus + +class Component(): + """A class for running regression tests on a component. + + "Component" refers to a Pacemaker component, such as the scheduler. + + :attribute name: The name of the component. + :type name: str + :attribute description: The description of the component. + :type description: str + :attribute requires_root: Whether the component's tests must be run + as root. + :type requires_root: bool + :attribute supports_valgrind: Whether the component's tests support + running under valgrind. + :type supports_valgrind: bool + :attribute cmd: The command to run the component's tests, along with + any required options. + :type cmd: list[str] + + :method run([verbose=False], [valgrind=False]): Run the component's + regression tests and return the result. + """ + + def __init__(self, name, description, test_home, requires_root=False, + supports_valgrind=False): + """Constructor for the :class:`Component` class. + + :param name: The name of the component. + :type name: str + :param description: The description of the component. + :type description: str + :param test_home: The directory where the component's tests + reside. + :type test_home: str + :param requires_root: Whether the component's tests must be run + as root. + :type requires_root: bool + :param supports_valgrind: Whether the component's tests support + running under valgrind. + :type supports_valgrind: bool + """ + self.name = name + self.description = description + self.requires_root = requires_root + self.supports_valgrind = supports_valgrind + + if self.name == 'pacemaker_remote': + self.cmd = [os.path.join(test_home, 'cts-exec'), '-R'] + else: + self.cmd = [os.path.join(test_home, 'cts-%s' % self.name)] + + def run(self, verbose=False, valgrind=False): + """Run the component's regression tests and return the result. + + :param verbose: Whether to increase test output verbosity. + :type verbose: bool + :param valgrind: Whether to run the test under valgrind. + :type valgrind: bool + :return: The exit code from the component's test suite. + :rtype: :class:`ExitStatus` + """ + print('Executing the %s regression tests' % self.name) + print('=' * 60) + + cmd = self.cmd + if self.requires_root and os.geteuid() != 0: + print('Enter the sudo password if prompted') + cmd = ['sudo'] + self.cmd + + if verbose: + cmd.append('--verbose') + + if self.supports_valgrind and valgrind: + cmd.append('--valgrind') + + try: + rc = ExitStatus(subprocess.call(cmd)) + except OSError as err: + error_print('Failed to execute %s tests: %s' % (self.name, err)) + rc = ExitStatus.NOT_INSTALLED + + print('=' * 60 + '\n\n') + return rc + + +class ComponentsArgAction(argparse.Action): + """A class to handle `components` arguments. + + This class handles special cases and cleans up the `components` + list. Specifically, it does the following: + * Enforce a default value of ['cli', 'scheduler']. + * Replace the 'all' alias with the components that it represents. + * Get rid of duplicates. + + The main motivation is that when the `choices` argument of + :meth:`parser.add_argument()` is specified, the `default` argument + must contain exactly one value (not `None` and not a list). We want + our default to be a list of components, namely `cli` and + `scheduler`. + """ + + def __call__(self, parser, namespace, values, option_string=None): + all_components = ['attrd', 'cli', 'exec', 'fencing', 'scheduler'] + default_components = ['cli', 'scheduler'] + + if not values: + setattr(namespace, self.dest, default_components) + return + + # If no argument is specified, the default gets passed as a + # string 'default' instead of as a list ['default']. Probably + # a bug in argparse. The below gives us a list. + if not isinstance(values, list): + values = [values] + + components = set(values) + + # If 'all', is found, replace it with the components it represents. + try: + components.remove('all') + components.update(set(all_components)) + except KeyError: + pass + + # Same for 'default' + try: + components.remove('default') + components.update(set(default_components)) + except KeyError: + pass + + setattr(namespace, self.dest, sorted(list(components))) + + +def error_print(msg): + """Print an error message. + + :param msg: Message to print. + :type msg: str + """ + print(' * ERROR: %s' % msg) + + +def run_components(components, verbose=False, valgrind=False): + """Run components' regression tests and report results for each. + + :param components: A list of names of components for which to run + tests. + :type components: list[:class:`Component`] + :return: :attr:`ExitStatus.OK` if all tests were successful, + :attr:`ExitStatus.ERROR` otherwise. + :rtype: :class:`ExitStatus` + """ + failed = [] + + for comp in components: + rc = comp.run(verbose, valgrind) + if rc != ExitStatus.OK: + error_print('%s regression tests failed (%s)' % (comp.name, rc)) + failed.append(comp.name) + + if failed: + print('Failed regression tests:', end='') + for comp in failed: + print(' %s' % comp, end='') + print() + return ExitStatus.ERROR + + return ExitStatus.OK + + +def main(): + """Run Pacemaker regression tests as specified by arguments.""" + try: + test_home = os.path.dirname(os.readlink(sys.argv[0])) + except OSError: + test_home = os.path.dirname(sys.argv[0]) + + # Available components + components = { + 'attrd': Component( + 'attrd', + 'Attribute manager', + test_home, + requires_root=True, + supports_valgrind=False, + ), + 'cli': Component( + 'cli', + 'Command-line tools', + test_home, + requires_root=False, + supports_valgrind=True, + ), + 'exec': Component( + 'exec', + 'Local resource agent executor', + test_home, + requires_root=True, + supports_valgrind=False, + ), + 'fencing': Component( + 'fencing', + 'Fencer', + test_home, + requires_root=True, + supports_valgrind=False, + ), + 'scheduler': Component( + 'scheduler', + 'Action scheduler', + test_home, + requires_root=False, + supports_valgrind=True, + ), + } + + if BuildOptions.REMOTE_ENABLED: + components['pacemaker_remote'] = Component( + 'pacemaker_remote', + 'Resource agent executor in remote mode', + test_home, + requires_root=True, + supports_valgrind=False, + ) + + # Build up program description + description = textwrap.dedent('''\ + Run Pacemaker regression tests. + + Available components (default components are 'cli scheduler'): + ''') + + for name, comp in sorted(components.items()): + description += '\n {:<20} {}'.format(name, comp.description) + + description += ( + '\n {:<20} Synonym for "cli exec fencing scheduler"'.format('all') + ) + + # Parse the arguments + parser = argparse.ArgumentParser( + description=description, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + choices = sorted(components.keys()) + ['all', 'default'] + + parser.add_argument('-V', '--verbose', action='store_true', + help='Increase test verbosity') + parser.add_argument('-v', '--valgrind', action='store_true', + help='Run test commands under valgrind') + parser.add_argument('components', nargs='*', choices=choices, + default='default', + action=ComponentsArgAction, metavar='COMPONENT', + help="One of the components to test, or 'all'") + args = parser.parse_args() + + # Run the tests + selected = [components[x] for x in args.components] + rc = run_components(selected, args.verbose, args.valgrind) + sys.exit(rc) + + +if __name__ == '__main__': + main() |