linux-rt-users.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: John Kacur <jkacur@redhat.com>
To: linux-rt-users <linux-rt-users@vger.kernel.org>
Cc: Clark Williams <williams@redhat.com>,
	Tomas Glozar <tglozar@redhat.com>, John Kacur <jkacur@redhat.com>,
	Claude <noreply@anthropic.com>
Subject: [PATCH 12/12] rteval: Add unit tests for --measurement-module argument
Date: Fri,  7 Nov 2025 13:26:35 -0500	[thread overview]
Message-ID: <20251107182645.19545-13-jkacur@redhat.com> (raw)
In-Reply-To: <20251107182645.19545-1-jkacur@redhat.com>

Add comprehensive unit testing infrastructure for the new
--measurement-module command-line argument.

Test coverage includes:
- Default config file selection behavior
- Override with cyclictest
- Override with timerlat
- No override when argument not provided
- Invalid module name rejection
- Exclusive module selection (only one enabled)

Testing infrastructure:
- Created tests/ directory following Python conventions
- Added test_measurement_module_selection.py with 6 test cases
- Added run_tests.sh script for automated test execution
- Added Makefile targets: 'make test' and 'make unittest'
- Updated 'make help' to document test targets

All tests pass successfully using Python's unittest framework.

Usage:
  make test
  ./run_tests.sh
  python3 tests/test_measurement_module_selection.py

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: John Kacur <jkacur@redhat.com>
---
 Makefile                                   |  10 ++
 run_tests.sh                               |  89 ++++++++++
 tests/test_measurement_module_selection.py | 195 +++++++++++++++++++++
 3 files changed, 294 insertions(+)
 create mode 100755 run_tests.sh
 create mode 100755 tests/test_measurement_module_selection.py

diff --git a/Makefile b/Makefile
index a250b18611b4..f17a434917ea 100644
--- a/Makefile
+++ b/Makefile
@@ -30,6 +30,12 @@ sysreport:
 	[ -d $(HERE)/run ] || mkdir run
 	$(PYTHON) rteval-cmd -D -v --workdir=$(HERE)/run --loaddir=$(HERE)/loadsource --duration=$(D) -i $(HERE)/rteval --sysreport
 
+unittest:
+	@echo "Running unit tests..."
+	./run_tests.sh
+
+test: unittest
+
 clean:
 	rm -f *~ rteval/*~ rteval/*.py[co] *.tar.bz2 *.tar.gz doc/*~
 
@@ -66,6 +72,8 @@ help:
 	@echo "rteval Makefile targets:"
 	@echo ""
 	@echo "        runit:     do a short testrun locally [default]"
+	@echo "        test:      run unit tests"
+	@echo "        unittest:  run unit tests (same as test)"
 	@echo "        tarfile:   create the source tarball"
 	@echo "        install:   install rteval locally"
 	@echo "        clean:     cleanup generated files"
@@ -82,3 +90,5 @@ tags:
 .PHONY: cleantags
 cleantags:
 	rm -f tags
+
+.PHONY: test unittest
diff --git a/run_tests.sh b/run_tests.sh
new file mode 100755
index 000000000000..c39d16feeea7
--- /dev/null
+++ b/run_tests.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Test runner for rteval unit tests
+#
+# This script runs all unit tests in the tests_progs/ directory
+# and provides a summary of results.
+#
+
+set -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Get the directory where this script is located
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+# Test results tracking
+TOTAL_TESTS=0
+PASSED_TESTS=0
+FAILED_TESTS=0
+
+echo "========================================="
+echo "Running rteval Unit Tests"
+echo "========================================="
+echo ""
+
+# Function to run a single test
+run_test() {
+    local test_file=$1
+    local test_name=$(basename "$test_file" .py)
+
+    echo -e "${YELLOW}Running: ${test_name}${NC}"
+    echo "---"
+
+    if python3 "$test_file"; then
+        echo -e "${GREEN}✓ PASSED: ${test_name}${NC}"
+        PASSED_TESTS=$((PASSED_TESTS + 1))
+    else
+        echo -e "${RED}✗ FAILED: ${test_name}${NC}"
+        FAILED_TESTS=$((FAILED_TESTS + 1))
+    fi
+
+    TOTAL_TESTS=$((TOTAL_TESTS + 1))
+    echo ""
+}
+
+# Find and run all test files in tests/
+if [ -d "tests" ]; then
+    # Run test_measurement_module_selection.py
+    if [ -f "tests/test_measurement_module_selection.py" ]; then
+        run_test "tests/test_measurement_module_selection.py"
+    fi
+
+    # Add more tests here as they are created
+    # Example:
+    # if [ -f "tests/test_another_feature.py" ]; then
+    #     run_test "tests/test_another_feature.py"
+    # fi
+else
+    echo -e "${RED}Error: tests/ directory not found${NC}"
+    exit 1
+fi
+
+# Print summary
+echo "========================================="
+echo "Test Summary"
+echo "========================================="
+echo "Total tests run: $TOTAL_TESTS"
+echo -e "Passed: ${GREEN}$PASSED_TESTS${NC}"
+if [ $FAILED_TESTS -gt 0 ]; then
+    echo -e "Failed: ${RED}$FAILED_TESTS${NC}"
+else
+    echo -e "Failed: $FAILED_TESTS"
+fi
+echo ""
+
+# Exit with appropriate code
+if [ $FAILED_TESTS -eq 0 ]; then
+    echo -e "${GREEN}✓ All tests passed!${NC}"
+    exit 0
+else
+    echo -e "${RED}✗ Some tests failed${NC}"
+    exit 1
+fi
diff --git a/tests/test_measurement_module_selection.py b/tests/test_measurement_module_selection.py
new file mode 100755
index 000000000000..2216cc668297
--- /dev/null
+++ b/tests/test_measurement_module_selection.py
@@ -0,0 +1,195 @@
+#!/usr/bin/python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Unit test for --measurement-module command-line argument
+#
+# This test verifies that the --measurement-module argument correctly
+# overrides config file settings to select between cyclictest and timerlat.
+#
+
+import sys
+import os
+import unittest
+import argparse
+
+# Add rteval to path
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from rteval import rtevalConfig
+from rteval.Log import Log
+
+
+class TestMeasurementModuleSelection(unittest.TestCase):
+    """Test suite for measurement module selection via command-line argument"""
+
+    def setUp(self):
+        """Set up test fixtures"""
+        self.logger = Log()
+        self.logger.SetLogVerbosity(Log.NONE)
+
+    def test_default_config_file_selection(self):
+        """Test that config file settings are loaded correctly by default"""
+        config = rtevalConfig.rtevalConfig(logger=self.logger)
+
+        # Load the rteval.conf file (which currently has timerlat enabled)
+        config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'rteval.conf')
+        if os.path.exists(config_file):
+            config.Load(config_file)
+            msrcfg = config.GetSection('measurement')
+
+            # Based on current rteval.conf, timerlat should be set to 'module'
+            timerlat_value = getattr(msrcfg, 'timerlat', None)
+            self.assertEqual(timerlat_value, 'module',
+                           "timerlat should be set to 'module' in rteval.conf")
+
+    def test_override_with_cyclictest(self):
+        """Test that --measurement-module cyclictest overrides config file"""
+        config = rtevalConfig.rtevalConfig(logger=self.logger)
+        config.AppendConfig('measurement', {
+            'timerlat': 'module',
+            'sysstat': 'module'
+        })
+
+        # Simulate the early argument parser
+        measurement_parser = argparse.ArgumentParser(add_help=False)
+        measurement_parser.add_argument('--measurement-module', dest='measurement_module',
+                                        type=str, choices=['cyclictest', 'timerlat'])
+        early_args, _ = measurement_parser.parse_known_args(['--measurement-module', 'cyclictest'])
+
+        # Apply the override logic (same as in rteval-cmd)
+        if early_args.measurement_module:
+            msrcfg = config.GetSection('measurement')
+            if 'cyclictest' in msrcfg:
+                msrcfg.cyclictest = None
+            if 'timerlat' in msrcfg:
+                msrcfg.timerlat = None
+            setattr(msrcfg, early_args.measurement_module, 'module')
+
+        # Verify the results
+        msrcfg = config.GetSection('measurement')
+        cyclictest_value = getattr(msrcfg, 'cyclictest', None)
+        timerlat_value = getattr(msrcfg, 'timerlat', None)
+
+        self.assertEqual(cyclictest_value, 'module',
+                        "cyclictest should be enabled after override")
+        self.assertNotEqual(timerlat_value, 'module',
+                           "timerlat should be disabled after override")
+
+    def test_override_with_timerlat(self):
+        """Test that --measurement-module timerlat overrides config file"""
+        config = rtevalConfig.rtevalConfig(logger=self.logger)
+        config.AppendConfig('measurement', {
+            'cyclictest': 'module',
+            'sysstat': 'module'
+        })
+
+        # Simulate the early argument parser
+        measurement_parser = argparse.ArgumentParser(add_help=False)
+        measurement_parser.add_argument('--measurement-module', dest='measurement_module',
+                                        type=str, choices=['cyclictest', 'timerlat'])
+        early_args, _ = measurement_parser.parse_known_args(['--measurement-module', 'timerlat'])
+
+        # Apply the override logic (same as in rteval-cmd)
+        if early_args.measurement_module:
+            msrcfg = config.GetSection('measurement')
+            if 'cyclictest' in msrcfg:
+                msrcfg.cyclictest = None
+            if 'timerlat' in msrcfg:
+                msrcfg.timerlat = None
+            setattr(msrcfg, early_args.measurement_module, 'module')
+
+        # Verify the results
+        msrcfg = config.GetSection('measurement')
+        cyclictest_value = getattr(msrcfg, 'cyclictest', None)
+        timerlat_value = getattr(msrcfg, 'timerlat', None)
+
+        self.assertEqual(timerlat_value, 'module',
+                        "timerlat should be enabled after override")
+        self.assertNotEqual(cyclictest_value, 'module',
+                           "cyclictest should be disabled after override")
+
+    def test_no_override_when_argument_not_provided(self):
+        """Test that config file settings remain when argument is not provided"""
+        config = rtevalConfig.rtevalConfig(logger=self.logger)
+        config.AppendConfig('measurement', {
+            'timerlat': 'module',
+            'sysstat': 'module'
+        })
+
+        # Simulate the early argument parser with no --measurement-module argument
+        measurement_parser = argparse.ArgumentParser(add_help=False)
+        measurement_parser.add_argument('--measurement-module', dest='measurement_module',
+                                        type=str, choices=['cyclictest', 'timerlat'])
+        early_args, _ = measurement_parser.parse_known_args([])
+
+        # Only apply override if argument was provided
+        if early_args.measurement_module:
+            msrcfg = config.GetSection('measurement')
+            if 'cyclictest' in msrcfg:
+                msrcfg.cyclictest = None
+            if 'timerlat' in msrcfg:
+                msrcfg.timerlat = None
+            setattr(msrcfg, early_args.measurement_module, 'module')
+
+        # Verify the results - should remain unchanged
+        msrcfg = config.GetSection('measurement')
+        timerlat_value = getattr(msrcfg, 'timerlat', None)
+
+        self.assertEqual(timerlat_value, 'module',
+                        "timerlat should remain enabled when no override is provided")
+
+    def test_argparse_rejects_invalid_module(self):
+        """Test that argparse rejects invalid module names"""
+        measurement_parser = argparse.ArgumentParser(add_help=False)
+        measurement_parser.add_argument('--measurement-module', dest='measurement_module',
+                                        type=str, choices=['cyclictest', 'timerlat'])
+
+        # This should raise SystemExit due to invalid choice
+        with self.assertRaises(SystemExit):
+            measurement_parser.parse_args(['--measurement-module', 'invalid'])
+
+    def test_both_modules_disabled_after_override(self):
+        """Test that both modules are disabled when one is selected"""
+        config = rtevalConfig.rtevalConfig(logger=self.logger)
+        config.AppendConfig('measurement', {
+            'cyclictest': 'module',
+            'timerlat': 'module',
+            'sysstat': 'module'
+        })
+
+        # Override with cyclictest
+        measurement_parser = argparse.ArgumentParser(add_help=False)
+        measurement_parser.add_argument('--measurement-module', dest='measurement_module',
+                                        type=str, choices=['cyclictest', 'timerlat'])
+        early_args, _ = measurement_parser.parse_known_args(['--measurement-module', 'cyclictest'])
+
+        if early_args.measurement_module:
+            msrcfg = config.GetSection('measurement')
+            if 'cyclictest' in msrcfg:
+                msrcfg.cyclictest = None
+            if 'timerlat' in msrcfg:
+                msrcfg.timerlat = None
+            setattr(msrcfg, early_args.measurement_module, 'module')
+
+        # Verify exactly one is enabled
+        msrcfg = config.GetSection('measurement')
+        cyclictest_value = getattr(msrcfg, 'cyclictest', None)
+        timerlat_value = getattr(msrcfg, 'timerlat', None)
+
+        enabled_count = sum([
+            1 if cyclictest_value == 'module' else 0,
+            1 if timerlat_value == 'module' else 0
+        ])
+
+        self.assertEqual(enabled_count, 1,
+                        "Exactly one measurement module should be enabled")
+
+
+def main():
+    """Run the test suite"""
+    # Run tests with verbosity
+    unittest.main(verbosity=2)
+
+
+if __name__ == '__main__':
+    main()
-- 
2.51.1


      parent reply	other threads:[~2025-11-07 18:27 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-11-07 18:26 [PATCH 00/12] rteval updates John Kacur
2025-11-07 18:26 ` [PATCH 01/12] rteval: Fix spelling of 'occurrences' in measurement modules John Kacur
2025-11-07 18:26 ` [PATCH 02/12] rteval: Fix typo in comment John Kacur
2025-11-07 18:26 ` [PATCH 03/12] rteval: Remove unused function remove_offline John Kacur
2025-11-07 18:26 ` [PATCH 04/12] rteval: timerlat: Fix typo in log message John Kacur
2025-11-07 18:26 ` [PATCH 05/12] rteval: cyclictest: Fix typo in comment John Kacur
2025-11-07 18:26 ` [PATCH 06/12] rteval: rtevalConfig: Remove redundant 'is True' comparison John Kacur
2025-11-07 18:26 ` [PATCH 07/12] rteval: Clean up MANIFEST.in and fix newnet.py copyright header John Kacur
2025-11-07 18:26 ` [PATCH 08/12] rteval: Add pyproject.toml for modern Python packaging John Kacur
2025-11-07 18:26 ` [PATCH 09/12] rteval: Improve argparse implementation and remove manual sys.argv parsing John Kacur
2025-11-07 18:26 ` [PATCH 10/12] rteval: timerlat: Add dma_latency option with default value of 0 John Kacur
2025-11-07 18:26 ` [PATCH 11/12] rteval: Add --measurement-module command-line argument John Kacur
2025-11-07 18:26 ` John Kacur [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251107182645.19545-13-jkacur@redhat.com \
    --to=jkacur@redhat.com \
    --cc=linux-rt-users@vger.kernel.org \
    --cc=noreply@anthropic.com \
    --cc=tglozar@redhat.com \
    --cc=williams@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).