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
prev 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).