From 1e535f80df19e952e1f1633ea233bbe6f89e0dc8 Mon Sep 17 00:00:00 2001 From: Alexandra Bara Date: Fri, 21 Nov 2025 10:21:25 -0600 Subject: [PATCH] functional test workflow + simple tests --- .github/workflows/code_quality_checks.yml | 2 +- .github/workflows/functional-test.yml | 30 +++++++ .github/workflows/unit-test.yml | 2 +- test/functional/__init__.py | 26 ++++++ test/functional/conftest.py | 57 ++++++++++++++ test/functional/test_cli_describe.py | 55 +++++++++++++ test/functional/test_cli_help.py | 83 ++++++++++++++++++++ test/functional/test_plugin_registry.py | 75 ++++++++++++++++++ test/functional/test_run_plugins.py | 96 +++++++++++++++++++++++ 9 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/functional-test.yml create mode 100644 test/functional/__init__.py create mode 100644 test/functional/conftest.py create mode 100644 test/functional/test_cli_describe.py create mode 100644 test/functional/test_cli_help.py create mode 100644 test/functional/test_plugin_registry.py create mode 100644 test/functional/test_run_plugins.py diff --git a/.github/workflows/code_quality_checks.yml b/.github/workflows/code_quality_checks.yml index 6bb149ee..e95e8769 100644 --- a/.github/workflows/code_quality_checks.yml +++ b/.github/workflows/code_quality_checks.yml @@ -11,7 +11,7 @@ on: jobs: pre-commit: runs-on: [ self-hosted ] - container: python:3.10 + container: python:3.9 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml new file mode 100644 index 00000000..8fd1fcf4 --- /dev/null +++ b/.github/workflows/functional-test.yml @@ -0,0 +1,30 @@ +name: Python Functional Tests + +on: + workflow_dispatch: + pull_request: + push: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + run_tests: + runs-on: [ self-hosted ] + container: python:3.9 + + steps: + - uses: actions/checkout@v3 + + - name: Install xmllint + run: | + apt-get update + apt-get install -y libxml2-utils bc + + - name: Install package and run functional tests + id: run_functional_tests + shell: bash + run: | + source ./dev-setup.sh + pytest test/functional -s --disable-warnings -v diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 7e4d38f6..7a4b17c7 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -12,7 +12,7 @@ permissions: jobs: run_tests: runs-on: [ self-hosted ] - container: python:3.10 + container: python:3.9 steps: - uses: actions/checkout@v3 diff --git a/test/functional/__init__.py b/test/functional/__init__.py new file mode 100644 index 00000000..711ec35f --- /dev/null +++ b/test/functional/__init__.py @@ -0,0 +1,26 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +############################################################################### +"""Functional tests for node-scraper.""" diff --git a/test/functional/conftest.py b/test/functional/conftest.py new file mode 100644 index 00000000..77ded955 --- /dev/null +++ b/test/functional/conftest.py @@ -0,0 +1,57 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +############################################################################### +"""Shared fixtures for functional tests.""" + +import subprocess +import sys +from typing import List + +import pytest + + +@pytest.fixture +def run_cli_command(): + """Fixture that returns a function to run CLI commands.""" + + def _run_command(args: List[str], check: bool = False): + """Run a node-scraper CLI command. + + Args: + args: List of command-line arguments + check: If True, raise CalledProcessError on non-zero exit + + Returns: + subprocess.CompletedProcess instance + """ + cmd = [sys.executable, "-m", "nodescraper.cli.cli"] + args + return subprocess.run( + cmd, + capture_output=True, + text=True, + check=check, + ) + + return _run_command diff --git a/test/functional/test_cli_describe.py b/test/functional/test_cli_describe.py new file mode 100644 index 00000000..52097a54 --- /dev/null +++ b/test/functional/test_cli_describe.py @@ -0,0 +1,55 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +############################################################################### +"""Functional tests for CLI describe command.""" + + +def test_describe_command_list_plugins(run_cli_command): + """Test that describe command can list all plugins.""" + result = run_cli_command(["describe", "plugin"]) + + assert result.returncode == 0 + assert len(result.stdout) > 0 + output = result.stdout.lower() + assert "available plugins" in output or "biosplugin" in output or "kernelplugin" in output + + +def test_describe_command_single_plugin(run_cli_command): + """Test that describe command can describe a single plugin.""" + result = run_cli_command(["describe", "plugin", "BiosPlugin"]) + + assert result.returncode == 0 + assert len(result.stdout) > 0 + output = result.stdout.lower() + assert "bios" in output + + +def test_describe_invalid_plugin(run_cli_command): + """Test that describe command handles invalid plugin gracefully.""" + result = run_cli_command(["describe", "plugin", "NonExistentPlugin"]) + + assert result.returncode != 0 + output = (result.stdout + result.stderr).lower() + assert "error" in output or "not found" in output or "invalid" in output diff --git a/test/functional/test_cli_help.py b/test/functional/test_cli_help.py new file mode 100644 index 00000000..f911eafd --- /dev/null +++ b/test/functional/test_cli_help.py @@ -0,0 +1,83 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +############################################################################### +"""Functional tests for node-scraper CLI help commands.""" + +import subprocess +import sys + + +def test_help_command(): + """Test that node-scraper -h displays help information.""" + result = subprocess.run( + [sys.executable, "-m", "nodescraper.cli.cli", "-h"], + capture_output=True, + text=True, + ) + + assert result.returncode == 0 + assert "usage:" in result.stdout.lower() + assert "node scraper" in result.stdout.lower() + assert "-h" in result.stdout or "--help" in result.stdout + + +def test_help_command_long_form(): + """Test that node-scraper --help displays help information.""" + result = subprocess.run( + [sys.executable, "-m", "nodescraper.cli.cli", "--help"], + capture_output=True, + text=True, + ) + + assert result.returncode == 0 + assert "usage:" in result.stdout.lower() + assert "node scraper" in result.stdout.lower() + + +def test_no_arguments(): + """Test that node-scraper with no arguments runs the default config.""" + result = subprocess.run( + [sys.executable, "-m", "nodescraper.cli.cli"], + capture_output=True, + text=True, + timeout=30, + ) + + assert len(result.stdout) > 0 or len(result.stderr) > 0 + output = (result.stdout + result.stderr).lower() + assert "plugin" in output or "nodescraper" in output + + +def test_help_shows_subcommands(): + """Test that help output includes available subcommands.""" + result = subprocess.run( + [sys.executable, "-m", "nodescraper.cli.cli", "-h"], + capture_output=True, + text=True, + ) + + assert result.returncode == 0 + output = result.stdout.lower() + assert "run-plugins" in output or "commands:" in output or "positional arguments:" in output diff --git a/test/functional/test_plugin_registry.py b/test/functional/test_plugin_registry.py new file mode 100644 index 00000000..77d352f7 --- /dev/null +++ b/test/functional/test_plugin_registry.py @@ -0,0 +1,75 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +############################################################################### +"""Functional tests for plugin registry and plugin loading.""" + +import inspect + +from nodescraper.pluginregistry import PluginRegistry + + +def test_plugin_registry_loads_plugins(): + """Test that PluginRegistry successfully loads built-in plugins.""" + registry = PluginRegistry() + + assert len(registry.plugins) > 0 + plugin_names = [name.lower() for name in registry.plugins.keys()] + expected_plugins = ["biosplugin", "kernelplugin", "osplugin"] + + for expected in expected_plugins: + assert expected in plugin_names + + +def test_plugin_registry_has_connection_managers(): + """Test that PluginRegistry loads connection managers.""" + registry = PluginRegistry() + + assert len(registry.connection_managers) > 0 + conn_names = [name.lower() for name in registry.connection_managers.keys()] + assert "inbandconnectionmanager" in conn_names + + +def test_plugin_registry_list_plugins(): + """Test that PluginRegistry stores plugins in a dictionary.""" + registry = PluginRegistry() + plugin_dict = registry.plugins + + assert isinstance(plugin_dict, dict) + assert len(plugin_dict) > 0 + assert all(isinstance(name, str) for name in plugin_dict.keys()) + assert all(inspect.isclass(cls) for cls in plugin_dict.values()) + + +def test_plugin_registry_get_plugin(): + """Test that PluginRegistry can retrieve a specific plugin.""" + registry = PluginRegistry() + plugin_names = list(registry.plugins.keys()) + assert len(plugin_names) > 0 + + first_plugin_name = plugin_names[0] + plugin = registry.plugins[first_plugin_name] + + assert plugin is not None + assert hasattr(plugin, "run") diff --git a/test/functional/test_run_plugins.py b/test/functional/test_run_plugins.py new file mode 100644 index 00000000..c8446f8b --- /dev/null +++ b/test/functional/test_run_plugins.py @@ -0,0 +1,96 @@ +############################################################################### +# +# MIT License +# +# Copyright (c) 2025 Advanced Micro Devices, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +############################################################################### +"""Functional tests for running individual plugins.""" + +import pytest + +from nodescraper.pluginregistry import PluginRegistry + + +@pytest.fixture(scope="module") +def all_plugins(): + """Get list of all available plugin names.""" + registry = PluginRegistry() + return sorted(registry.plugins.keys()) + + +def test_plugin_registry_has_plugins(all_plugins): + """Verify that plugins are available for testing.""" + assert len(all_plugins) > 0 + + +@pytest.mark.parametrize( + "plugin_name", + [ + "BiosPlugin", + "CmdlinePlugin", + "DimmPlugin", + "DkmsPlugin", + "DmesgPlugin", + "JournalPlugin", + "KernelPlugin", + "KernelModulePlugin", + "MemoryPlugin", + "NvmePlugin", + "OsPlugin", + "PackagePlugin", + "ProcessPlugin", + "RocmPlugin", + "StoragePlugin", + "SysctlPlugin", + "SyslogPlugin", + "UptimePlugin", + ], +) +def test_run_individual_plugin(run_cli_command, plugin_name, tmp_path): + """Test running each plugin individually.""" + log_path = str(tmp_path / f"logs_{plugin_name}") + result = run_cli_command(["--log-path", log_path, "run-plugins", plugin_name], check=False) + + assert result.returncode in [0, 1, 2] + output = result.stdout + result.stderr + assert len(output) > 0 + assert plugin_name.lower() in output.lower() + + +def test_run_all_plugins_together(run_cli_command, all_plugins, tmp_path): + """Test running all plugins together.""" + plugins_to_run = all_plugins[:3] + log_path = str(tmp_path / "logs_multiple") + result = run_cli_command(["--log-path", log_path, "run-plugins"] + plugins_to_run, check=False) + + assert result.returncode in [0, 1, 2] + output = result.stdout + result.stderr + assert len(output) > 0 + + +def test_run_plugin_with_invalid_name(run_cli_command): + """Test that running a non-existent plugin fails gracefully.""" + result = run_cli_command(["run-plugins", "NonExistentPlugin"], check=False) + + assert result.returncode != 0 + output = (result.stdout + result.stderr).lower() + assert "error" in output or "invalid" in output or "not found" in output