diff --git a/cts/Makefile.am b/cts/Makefile.am index 598ae32b9b..ba61984f17 100644 --- a/cts/Makefile.am +++ b/cts/Makefile.am @@ -1,76 +1,76 @@ # -# Copyright 2001-2023 the Pacemaker project contributors +# Copyright 2001-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # +include $(top_srcdir)/mk/python.mk + MAINTAINERCLEANFILES = Makefile.in # Test commands and globally applicable test files should be in $(testdir), # and command-specific test data should be in a command-specific subdirectory. testdir = $(datadir)/$(PACKAGE)/tests test_SCRIPTS = cts-attrd \ cts-cli \ cts-exec \ cts-fencing \ cts-lab \ cts-regression \ cts-scheduler dist_test_DATA = README.md \ valgrind-pcmk.suppressions clidir = $(testdir)/cli dist_cli_DATA = $(wildcard cli/*.xml cli/*.exp) ctsdir = $(datadir)/$(PACKAGE)/tests/cts cts_SCRIPTS = cts # Commands intended to be run only via other commands halibdir = $(CRM_DAEMON_DIR) dist_halib_SCRIPTS = cts-log-watcher noinst_SCRIPTS = cluster_test .PHONY: scheduler-list scheduler-list: @for T in "$(srcdir)"/scheduler/xml/*.xml; do \ echo $$(basename $$T .xml); \ done CLEANFILES = $(builddir)/.regression.failed.diff .PHONY: clean-local clean-local: rm -f scheduler/*/*.pe SUBDIRS = benchmark \ scheduler \ support .PHONY: cts-support-install cts-support-install: $(MAKE) $(AM_MAKEFLAGS) -C support cts-support $(builddir)/support/cts-support install .PHONY: cts-support-uninstall cts-support-uninstall: $(MAKE) $(AM_MAKEFLAGS) -C support cts-support $(builddir)/support/cts-support uninstall # Everything listed here is a python script, typically generated from a .in file # (though that is not a requirement). We want to run pylint on all of these # things after they've been built. -python_files = cts-attrd \ - cts-exec \ - cts-fencing \ - cts-lab \ - cts-log-watcher \ - cts-regression \ +python_files = cts-attrd \ + cts-exec \ + cts-fencing \ + cts-lab \ + cts-log-watcher \ + cts-regression \ cts-scheduler -.PHONY: pylint -pylint: $(python_files) - PYTHONPATH=$(top_builddir)/python pylint --rcfile $(top_srcdir)/python/pylintrc $(python_files) +PYCHECKFILES ?= $(python_files) diff --git a/cts/cts-attrd.in b/cts/cts-attrd.in index c9a219d326..29f577123f 100644 --- a/cts/cts-attrd.in +++ b/cts/cts-attrd.in @@ -1,367 +1,366 @@ #!@PYTHON@ -""" Regression tests for Pacemaker's attribute daemon -""" +"""Regression tests for Pacemaker's attribute daemon.""" # pylint doesn't like the module name "cts-attrd" which is an invalid complaint for this file # but probably something we want to continue warning about elsewhere # pylint: disable=invalid-name # pacemaker imports need to come after we modify sys.path, which pylint will complain about. # pylint: disable=wrong-import-position __copyright__ = "Copyright 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 tempfile # 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") # pylint: disable=comparison-of-constants,comparison-with-itself,condition-evals-to-constant 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 from pacemaker._cts.corosync import Corosync from pacemaker._cts.process import killall, exit_if_proc_running from pacemaker._cts.test import Test, Tests TEST_DIR = sys.path[0] -def update_path(): - """ Set the PATH environment variable appropriately for the tests """ +def update_path(): + """Set the PATH environment variable appropriately for the tests.""" new_path = os.environ['PATH'] if os.path.exists("%s/cts-attrd.in" % TEST_DIR): # pylint: disable=protected-access print("Running tests from the source tree: %s (%s)" % (BuildOptions._BUILD_DIR, TEST_DIR)) # For pacemaker-attrd new_path = "%s/daemons/attrd:%s" % (BuildOptions._BUILD_DIR, new_path) else: print("Running tests from the install tree: %s (not %s)" % (BuildOptions.DAEMON_DIR, TEST_DIR)) # For pacemaker-attrd new_path = "%s:%s" % (BuildOptions.DAEMON_DIR, new_path) print('Using PATH="%s"' % new_path) os.environ['PATH'] = new_path class AttributeTest(Test): - """ Executor for a single test """ + """Executor for a single test.""" def __init__(self, name, description, **kwargs): + """ + Create a new AttributeTest instance. + + Arguments: + name -- A unique name for this test. This can be used on the + command line to specify that only a specific test should + be executed. + description -- A meaningful description for the test. + """ Test.__init__(self, name, description, **kwargs) self._daemon_location = "pacemaker-attrd" self._enable_corosync = True def _kill_daemons(self): killall([self._daemon_location]) def _start_daemons(self): if self.verbose: print("Starting %s" % self._daemon_location) cmd = [self._daemon_location, "-s", "-l", self.logpath] # pylint: disable=consider-using-with self._daemon_process = subprocess.Popen(cmd) class AttributeTests(Tests): - """ Collection of all attribute regression tests """ + """Collection of all attribute regression tests.""" def __init__(self, **kwargs): + """Create a new AttributeTests instance.""" Tests.__init__(self, **kwargs) self._corosync = Corosync(self.verbose, self.logdir, "cts-attrd") def new_test(self, name, description): - """ Create a named test """ - + """Create a named test.""" test = AttributeTest(name, description, verbose=self.verbose, logdir=self.logdir) self._tests.append(test) return test def setup_environment(self, use_corosync): - """ Prepare the host before executing any tests """ - + """Prepare the host before executing any tests.""" if use_corosync: self._corosync.start(kill_first=True) def cleanup_environment(self, use_corosync): - """ Clean up the host after executing desired tests """ - + """Clean up the host after executing desired tests.""" if use_corosync: self._corosync.stop() def build_basic_tests(self): - """ Add basic tests - setting, querying, updating, and deleting attributes """ - + """Add basic tests - setting, querying, updating, and deleting attributes.""" test = self.new_test("set_attr_1", "Set and query an attribute") test.add_cmd("attrd_updater", "--name AAA -U 111 --output-as=xml") test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml", "name=\"AAA\" value=\"111\"") test.add_log_pattern(r"Setting AAA\[.*\] in instance_attributes: \(unset\) -> 111", regex=True) # Setting the delay on an attribute that doesn't exist fails, but the failure is # not passed back to attrd_updater. test = self.new_test("set_attr_2", "Set an attribute's delay") test.add_cmd("attrd_updater", "--name AAA -Y -d 5 --output-as=xml") test.add_log_pattern(r"Processed update-delay request from client .*: Error \(Attribute AAA does not exist\)", regex=True) test = self.new_test("set_attr_3", "Set and query an attribute's delay and value") test.add_cmd("attrd_updater", "--name AAA -B 111 -d 5 --output-as=xml") test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml", "name=\"AAA\" value=\"111\"") test.add_log_pattern(r"Setting AAA\[.*\] in instance_attributes: \(unset\) -> 111 \| from .* with 5s write delay", regex=True) test = self.new_test("set_attr_4", "Update an attribute that does not exist with a delay") test.add_cmd("attrd_updater", "--name BBB -U 999 -d 10 --output-as=xml") test.add_cmd_check_stdout("attrd_updater", "--name BBB -Q --output-as=xml", "name=\"BBB\" value=\"999\"") test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: \(unset\) -> 999 \| from .* with 10s write delay", regex=True) test = self.new_test("update_attr_1", "Update an attribute that already exists") test.add_cmd("attrd_updater", "--name BBB -U 222 --output-as=xml") test.add_cmd("attrd_updater", "--name BBB -U 333 --output-as=xml") test.add_cmd_check_stdout("attrd_updater", "--name BBB -Q --output-as=xml", "name=\"BBB\" value=\"333\"") test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: \(unset\) -> 222", regex=True) test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: 222 -> 333", regex=True) test = self.new_test("update_attr_2", "Update an attribute using a delay other than its default") test.add_cmd("attrd_updater", "--name BBB -U 777 -d 10 --output-as=xml") test.add_cmd("attrd_updater", "--name BBB -U 888 -d 7 --output-as=xml") test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: 777 -> 888 \| from .* with 10s write delay", regex=True) test = self.new_test("update_attr_delay_1", "Update the delay of an attribute that already exists") test.add_cmd("attrd_updater", "--name BBB -U 222 --output-as=xml") test.add_cmd("attrd_updater", "--name BBB -Y -d 5 --output-as=xml") test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: \(unset\) -> 222", regex=True) test.add_log_pattern("Update attribute BBB delay to 5000ms (5)") test = self.new_test("update_attr_delay_2", "Update the delay and value of an attribute that already exists") test.add_cmd("attrd_updater", "--name BBB -U 222 --output-as=xml") test.add_cmd("attrd_updater", "--name BBB -B 333 -d 5 --output-as=xml") test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: \(unset\) -> 222", regex=True) test.add_log_pattern("Update attribute BBB delay to 5000ms (5)") test.add_log_pattern(r"Setting BBB\[.*\] in instance_attributes: 222 -> 333", regex=True) test = self.new_test("missing_attr_1", "Query an attribute that does not exist") test.add_cmd_expected_fail("attrd_updater", "--name NOSUCH --output-as=xml", ExitStatus.CONFIG) test = self.new_test("delete_attr_1", "Delete an existing attribute") test.add_cmd("attrd_updater", "--name CCC -U 444 --output-as=xml") test.add_cmd("attrd_updater", "--name CCC -D --output-as=xml") test.add_log_pattern(r"Setting CCC\[.*\] in instance_attributes: \(unset\) -> 444", regex=True) test.add_log_pattern(r"Setting CCC\[.*\] in instance_attributes: 444 -> \(unset\)", regex=True) test = self.new_test("missing_attr_2", "Delete an attribute that does not exist") test.add_cmd("attrd_updater", "--name NOSUCH2 -D --output-as=xml") test = self.new_test("attr_in_set_1", "Set and query an attribute in a specific set") test.add_cmd("attrd_updater", "--name DDD -U 555 --set=foo --output-as=xml") test.add_cmd_check_stdout("attrd_updater", "--name DDD -Q --output-as=xml", "name=\"DDD\" value=\"555\"") test.add_log_pattern("Processed 1 private change for DDD (set foo)") def build_multiple_query_tests(self): - """ Add tests that set and query an attribute across multiple nodes """ - + """Add tests that set and query an attribute across multiple nodes.""" # NOTE: These tests make use of the fact that nothing in attrd actually # cares about whether a node exists when you set or query an attribute. # It just keeps creating new hash tables for each node you ask it about. test = self.new_test("multi_query_1", "Query an attribute set across multiple nodes") test.add_cmd("attrd_updater", "--name AAA -U 111 --node cluster1 --output-as=xml") test.add_cmd("attrd_updater", "--name AAA -U 222 --node cluster2 --output-as=xml") test.add_cmd_check_stdout("attrd_updater", "--name AAA -QA --output-as=xml", r"""\n.*""") test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --node=cluster1 --output-as=xml", """""") test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --node=cluster2 --output-as=xml", """""") test.add_cmd_check_stdout("attrd_updater", "--name AAA -QA --output-as=xml", r"""\n.*""", env={"OCF_RESKEY_CRM_meta_on_node": "cluster1"}) test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml", """""", env={"OCF_RESKEY_CRM_meta_on_node": "cluster1"}) test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --node=cluster2 --output-as=xml", """""", env={"OCF_RESKEY_CRM_meta_on_node": "cluster1"}) def build_regex_tests(self): - """ Add tests that use regexes """ - + """Add tests that use regexes.""" test = self.new_test("regex_update_1", "Update attributes using a regex") test.add_cmd("attrd_updater", "--name AAA -U 111 --output-as=xml") test.add_cmd("attrd_updater", "--name ABB -U 222 --output-as=xml") test.add_cmd("attrd_updater", "-P 'A.*' -U 333 --output-as=xml") test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml", "name=\"AAA\" value=\"333\"") test.add_cmd_check_stdout("attrd_updater", "--name ABB -Q --output-as=xml", "name=\"ABB\" value=\"333\"") test.add_log_pattern(r"Setting AAA\[.*\] in instance_attributes: \(unset\) -> 111", regex=True) test.add_log_pattern(r"Setting ABB\[.*\] in instance_attributes: \(unset\) -> 222", regex=True) test.add_log_pattern(r"Setting ABB\[.*\] in instance_attributes: 222 -> 333", regex=True) test.add_log_pattern(r"Setting AAA\[.*\] in instance_attributes: 111 -> 333", regex=True) test = self.new_test("regex_delete_1", "Delete attributes using a regex") test.add_cmd("attrd_updater", "--name XAX -U 444 --output-as=xml") test.add_cmd("attrd_updater", "--name XBX -U 555 --output-as=xml") test.add_cmd("attrd_updater", "-P 'X[A|B]X' -D --output-as=xml") test.add_log_pattern(r"Setting XAX\[.*\] in instance_attributes: \(unset\) -> 444", regex=True) test.add_log_pattern(r"Setting XBX\[.*\] in instance_attributes: \(unset\) -> 555", regex=True) test.add_log_pattern(r"Setting XBX\[.*\] in instance_attributes: 555 -> \(unset\)", regex=True) test.add_log_pattern(r"Setting XAX\[.*\] in instance_attributes: 444 -> \(unset\)", regex=True) def build_utilization_tests(self): - """ Add tests that involve utilization attributes """ - + """Add tests that involve utilization attributes.""" test = self.new_test("utilization_1", "Set and query a utilization attribute") test.add_cmd("attrd_updater", "--name AAA -U ABC -z --output-as=xml") test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml", "name=\"AAA\" value=\"ABC\"") test.add_log_pattern(r"Setting AAA\[.*\] in utilization: \(unset\) -> ABC", regex=True) def build_sync_point_tests(self): - """ Add tests that involve sync points """ - + """Add tests that involve sync points.""" test = self.new_test("local_sync_point", "Wait for a local sync point") test.add_cmd("attrd_updater", "--name AAA -U 123 --wait=local --output-as=xml") test.add_cmd_check_stdout("attrd_updater", "--name AAA -Q --output-as=xml", "name=\"AAA\" value=\"123\"") test.add_log_pattern(r"Alerting client .* for reached local sync point", regex=True) test = self.new_test("cluster_sync_point", "Wait for a cluster-wide sync point") test.add_cmd("attrd_updater", "--name BBB -U 456 --wait=cluster --output-as=xml") test.add_cmd_check_stdout("attrd_updater", "--name BBB -Q --output-as=xml", "name=\"BBB\" value=\"456\"") test.add_log_pattern(r"Alerting client .* for reached cluster sync point", regex=True) def build_options(): - """ Handle command line arguments """ - + """Handle command line arguments.""" parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description="Run pacemaker-attrd regression tests", epilog="Example: Run only the test 'start_stop'\n" "\t " + sys.argv[0] + " --run-only start_stop\n\n" "Example: Run only the tests with the string 'systemd' present in them\n" "\t " + sys.argv[0] + " --run-only-pattern systemd") parser.add_argument("-l", "--list-tests", action="store_true", help="Print out all registered tests") parser.add_argument("-p", "--run-only-pattern", metavar='PATTERN', help="Run only tests matching the given pattern") parser.add_argument("-r", "--run-only", metavar='TEST', help="Run a specific test") parser.add_argument("-V", "--verbose", action="store_true", help="Verbose output") args = parser.parse_args() return args def main(): - """ Run attrd regression tests as specified by arguments """ - + """Run attrd regression tests as specified by arguments.""" update_path() # Ensure all command output is in portable locale for comparison os.environ['LC_ALL'] = "C" opts = build_options() exit_if_proc_running("pacemaker-attrd") # Create a temporary directory for log files (the directory and its # contents will automatically be erased when done) with tempfile.TemporaryDirectory(prefix="cts-attrd-") as logdir: tests = AttributeTests(verbose=opts.verbose, logdir=logdir) tests.build_basic_tests() tests.build_multiple_query_tests() tests.build_regex_tests() tests.build_utilization_tests() tests.build_sync_point_tests() if opts.list_tests: tests.print_list() sys.exit(ExitStatus.OK) print("Starting ...") try: tests.setup_environment(True) except TimeoutError: print("corosync did not start in time, exiting") sys.exit(ExitStatus.TIMEOUT) if opts.run_only_pattern: tests.run_tests_matching(opts.run_only_pattern) tests.print_results() elif opts.run_only: tests.run_single(opts.run_only) tests.print_results() else: tests.run_tests() tests.print_results() tests.cleanup_environment(True) tests.exit() if __name__ == "__main__": main() diff --git a/python/Makefile.am b/mk/python.mk similarity index 58% copy from python/Makefile.am copy to mk/python.mk index d54c2ddb87..8b8fc408e3 100644 --- a/python/Makefile.am +++ b/mk/python.mk @@ -1,39 +1,27 @@ # -# Copyright 2023-2024 the Pacemaker project contributors +# Copyright 2024 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # -MAINTAINERCLEANFILES = Makefile.in - -EXTRA_DIST = pylintrc - -SUBDIRS = pacemaker \ - tests - -.PHONY: check-local -check-local: - if [ "x$(top_srcdir)" != "x$(top_builddir)" ]; then \ - cp -r $(top_srcdir)/python/* $(abs_top_builddir)/python/; \ - fi - PYTHONPATH=$(top_builddir)/python $(PYTHON) -m unittest discover -v -s $(top_builddir)/python/tests - .PHONY: pylint -pylint: - pylint $(SUBDIRS) +pylint: $(PYCHECKFILES) + PYTHONPATH=$(abs_top_builddir)/python \ + pylint --rcfile $(top_srcdir)/python/pylintrc $(PYCHECKFILES) # Disabled warnings: # W503 - Line break occurred before a binary operator # (newer versions of pyflake and PEP8 want line breaks after binary # operators, but older versions still suggest before) # E501 - Line too long # # Disable unused imports on __init__.py files (we likely just have them # there for re-exporting). # Disable docstrings warnings on unit tests. .PHONY: pyflake -pyflake: - flake8 --ignore=W503,E501 --per-file-ignores="__init__.py:F401 tests/*:D100,D101,D102,D104" $(SUBDIRS) +pyflake: $(PYCHECKFILES) + PYTHONPATH=$(abs_top_builddir)/python \ + flake8 --ignore=W503,E501 --per-file-ignores="__init__.py:F401 tests/*:D100,D101,D102,D104" $(PYCHECKFILES) diff --git a/python/Makefile.am b/python/Makefile.am index d54c2ddb87..558c2e91f9 100644 --- a/python/Makefile.am +++ b/python/Makefile.am @@ -1,39 +1,26 @@ # # Copyright 2023-2024 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # +include $(top_srcdir)/mk/python.mk + MAINTAINERCLEANFILES = Makefile.in EXTRA_DIST = pylintrc SUBDIRS = pacemaker \ tests +PYCHECKFILES ?= $(SUBDIRS) + .PHONY: check-local check-local: if [ "x$(top_srcdir)" != "x$(top_builddir)" ]; then \ cp -r $(top_srcdir)/python/* $(abs_top_builddir)/python/; \ fi PYTHONPATH=$(top_builddir)/python $(PYTHON) -m unittest discover -v -s $(top_builddir)/python/tests - -.PHONY: pylint -pylint: - pylint $(SUBDIRS) - -# Disabled warnings: -# W503 - Line break occurred before a binary operator -# (newer versions of pyflake and PEP8 want line breaks after binary -# operators, but older versions still suggest before) -# E501 - Line too long -# -# Disable unused imports on __init__.py files (we likely just have them -# there for re-exporting). -# Disable docstrings warnings on unit tests. -.PHONY: pyflake -pyflake: - flake8 --ignore=W503,E501 --per-file-ignores="__init__.py:F401 tests/*:D100,D101,D102,D104" $(SUBDIRS)