[PATCH 5/5] tuna: Add tests for converter functions and update test README
John Kacur <[email protected]> Thu, 11 Jun 2026 16:38:32 -0400
| Newsgroups | org.kernel.vger.linux-rt-users |
|---|---|
| Message-ID | <[email protected]> |
Add comprehensive unit tests for argument converter functions: - threadstring_to_list(): 7 tests covering empty strings, numeric PIDs, duplicates, pattern matching, exact names, and mixed inputs - irqstring_to_list(): 4 tests covering empty strings, numeric IRQs, and duplicates - socketstring_to_list(): 1 test verifying function exists Tests use MockPidStats to avoid requiring real procfs access, allowing them to run without root privileges and in CI/CD environments. Update tests/README.md with comprehensive documentation: - How to run tests via make and unittest directly - Examples for running specific test files, classes, and methods - Explanation of why tests don't require root privileges - Description of all current tests (18 total: 6 EPERM + 12 converter) - Expected output showing successful test run - Updated requirements and test organization All 18 tests pass successfully without requiring root access. Assisted-by: Claude:claude-sonnet-4-5 Signed-off-by: John Kacur <[email protected]> --- tests/README.md | 85 ++++++++++++++++++++- tests/test_converters.py | 160 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 tests/test_converters.py diff --git a/tests/README.md b/tests/README.md index aceb38d6f8f5..9f85e6fe97c2 100644 --- a/tests/README.md +++ b/tests/README.md @@ -33,12 +33,22 @@ make test-eperm python3 -m unittest tests.test_eperm_handling -v ``` +### Specific Test Class + +Run a specific test class: + +```bash +python3 -m unittest tests.test_converters.TestThreadstringToList -v +python3 -m unittest tests.test_converters.TestIrqstringToList -v +``` + ### Specific Test Method Run a single test method: ```bash -python3 -m unittest tests.test_eperm_handling.TestEPERMHandling.test_eperm_constant_exists -v +python3 -m unittest tests.test_eperm_handling.TestEPERMHandling.test_set_affinity_eperm -v +python3 -m unittest tests.test_converters.TestThreadstringToList.test_pattern_with_mock_ps -v ``` ## Test Organization @@ -50,9 +60,41 @@ Tests are organized using Python's `unittest` framework. Each test file contains ### Current Tests -- **test_eperm_handling.py** - Tests for EPERM error handling in `isolate_cpus()` +- **test_eperm_handling.py** (6 tests) - Tests for EPERM error handling - Verifies that Permission Denied errors are handled gracefully - - Ensures tuna continues processing when it can't set affinity on protected processes + - Tests reading and setting affinity, scheduler, and priority on PID 1 + - Ensures tuna handles permission errors without crashing + +- **test_converters.py** (12 tests) - Tests for argument converter functions + - Tests `threadstring_to_list()`: Converting thread specifications to PID lists + - Tests `irqstring_to_list()`: Converting IRQ specifications to IRQ number lists + - Tests `socketstring_to_list()`: Socket string conversion + - Uses MockPidStats to test without requiring real process access + +### Why Tests Don't Require Root + +The tuna program requires root privileges for operations like: +- Setting CPU affinity on other processes +- Changing scheduler policies and priorities +- Moving IRQs to different CPUs +- Isolating CPUs (system-wide CPU management) + +However, the tests are designed to run without root privileges: + +1. **EPERM tests** verify that permission errors are handled gracefully when running as non-root + - They intentionally trigger permission errors by trying to modify PID 1 + - Success means the error was caught and handled properly + +2. **Converter tests** use mock objects instead of real system access + - Test pure logic: string parsing and conversion + - MockPidStats provides test data without accessing /proc + - No actual process manipulation occurs + +**Benefits:** +- Developers can run tests without `sudo` +- Tests run in CI/CD environments without elevated privileges +- Tests are fast and don't affect the running system +- Tests verify both happy path (with mocks) and error path (with real EPERM) ## Writing New Tests @@ -125,7 +167,42 @@ self.assertRaises(Exception, fn) # fn() raises Exception ## Requirements - Python 3.6 or later -- No additional packages required (uses Python standard library) +- python3-procfs package (for importing procfs module used by converters) +- No other external packages required (uses Python standard library and mocks) + +## Expected Output + +A successful test run with all 18 tests should look like: + +``` +Running tuna unit tests... + +test_get_affinity_eperm (test_eperm_handling.TestEPERMHandling) ... ok +test_get_priority_eperm (test_eperm_handling.TestEPERMHandling) ... ok +test_get_scheduler_eperm (test_eperm_handling.TestEPERMHandling) ... ok +test_set_affinity_eperm (test_eperm_handling.TestEPERMHandling) ... ok +test_set_priority_eperm (test_eperm_handling.TestEPERMHandling) ... ok +test_set_scheduler_eperm (test_eperm_handling.TestEPERMHandling) ... ok +test_duplicate_irqs_deduplicated (test_converters.TestIrqstringToList) ... ok +test_empty_string (test_converters.TestIrqstringToList) ... ok +test_multiple_numeric_irqs (test_converters.TestIrqstringToList) ... ok +test_single_numeric_irq (test_converters.TestIrqstringToList) ... ok +test_socketstring_exists (test_converters.TestSocketstringToList) ... ok +test_duplicate_pids_deduplicated (test_converters.TestThreadstringToList) ... ok +test_empty_string (test_converters.TestThreadstringToList) ... ok +test_exact_name_with_mock_ps (test_converters.TestThreadstringToList) ... ok +test_mixed_numeric_and_pattern (test_converters.TestThreadstringToList) ... ok +test_multiple_numeric_pids (test_converters.TestThreadstringToList) ... ok +test_pattern_with_mock_ps (test_converters.TestThreadstringToList) ... ok +test_single_numeric_pid (test_converters.TestThreadstringToList) ... ok + +---------------------------------------------------------------------- +Ran 18 tests in 0.XXXs + +OK + +All tests passed! +``` ## Test Types diff --git a/tests/test_converters.py b/tests/test_converters.py new file mode 100644 index 000000000000..26ead9d43049 --- /dev/null +++ b/tests/test_converters.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 John Kacur +# SPDX-License-Identifier: GPL-2.0-only +""" +Unit tests for argument converter functions. + +Tests verify that threadstring_to_list, irqstring_to_list, and +socketstring_to_list properly convert string arguments to lists. +""" + +import unittest +import sys +import os + +# Add parent directory to path to import tuna-cmd +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Import the converter functions from tuna-cmd +import importlib.util +spec = importlib.util.spec_from_file_location("tuna_cmd", os.path.join(os.path.dirname(os.path.dirname(__file__)), "tuna-cmd.py")) +tuna_cmd = importlib.util.module_from_spec(spec) + + +class MockPidStats: + """Mock procfs.pidstats() for testing""" + + def __init__(self, pids=None): + """Initialize with dict of pid -> info""" + self.pids = pids or {} + + def find_by_regex(self, pattern): + """Mock find_by_regex - returns PIDs based on pattern""" + import re + pattern_str = str(pattern) + # fnmatch.translate('systemd*') creates a pattern matching 'systemd' followed by anything + # fnmatch.translate('systemd') creates a pattern matching exactly 'systemd' + + # Check if it's a wildcard pattern (contains .* from fnmatch.translate) + if '.*' in pattern.pattern: + # This is 'systemd*' or similar - return multiple PIDs + return [100, 101] + else: + # This is exact match 'systemd' - return single PID + return [100] + + def find_by_name(self, name): + """Mock find_by_name - exact name match""" + if name == 'systemd': + return [100] + return [] + + +class TestThreadstringToList(unittest.TestCase): + """Test cases for threadstring_to_list() function""" + + @classmethod + def setUpClass(cls): + """Load the tuna-cmd module""" + spec.loader.exec_module(tuna_cmd) + + def test_empty_string(self): + """Test empty string returns empty list and False""" + result, match_requested = tuna_cmd.threadstring_to_list('') + self.assertEqual(result, []) + self.assertFalse(match_requested) + + def test_single_numeric_pid(self): + """Test single numeric PID""" + result, match_requested = tuna_cmd.threadstring_to_list('123') + self.assertEqual(result, [123]) + self.assertTrue(match_requested) + + def test_multiple_numeric_pids(self): + """Test multiple comma-separated numeric PIDs""" + result, match_requested = tuna_cmd.threadstring_to_list('1,2,3') + self.assertEqual(sorted(result), [1, 2, 3]) + self.assertTrue(match_requested) + + def test_duplicate_pids_deduplicated(self): + """Test duplicate PIDs are deduplicated""" + result, match_requested = tuna_cmd.threadstring_to_list('1,2,1,3,2') + self.assertEqual(sorted(result), [1, 2, 3]) + self.assertTrue(match_requested) + + def test_pattern_with_mock_ps(self): + """Test pattern matching with mock ps""" + mock_ps = MockPidStats() + result, match_requested = tuna_cmd.threadstring_to_list('systemd*', mock_ps) + self.assertEqual(sorted(result), [100, 101]) + self.assertTrue(match_requested) + + def test_exact_name_with_mock_ps(self): + """Test exact name matching with mock ps""" + mock_ps = MockPidStats() + result, match_requested = tuna_cmd.threadstring_to_list('systemd', mock_ps) + self.assertEqual(result, [100]) + self.assertTrue(match_requested) + + def test_mixed_numeric_and_pattern(self): + """Test mixed numeric PIDs and patterns""" + mock_ps = MockPidStats() + result, match_requested = tuna_cmd.threadstring_to_list('1,systemd*,5', mock_ps) + self.assertIn(1, result) + self.assertIn(5, result) + self.assertIn(100, result) + self.assertIn(101, result) + self.assertTrue(match_requested) + + +class TestIrqstringToList(unittest.TestCase): + """Test cases for irqstring_to_list() function""" + + @classmethod + def setUpClass(cls): + """Load the tuna-cmd module""" + spec.loader.exec_module(tuna_cmd) + + def test_empty_string(self): + """Test empty string returns empty list and False""" + result, match_requested = tuna_cmd.irqstring_to_list('') + self.assertEqual(result, []) + self.assertFalse(match_requested) + + def test_single_numeric_irq(self): + """Test single numeric IRQ""" + result, match_requested = tuna_cmd.irqstring_to_list('42') + self.assertEqual(result, [42]) + self.assertTrue(match_requested) + + def test_multiple_numeric_irqs(self): + """Test multiple comma-separated numeric IRQs""" + result, match_requested = tuna_cmd.irqstring_to_list('1,2,42') + self.assertEqual(sorted(result), [1, 2, 42]) + self.assertTrue(match_requested) + + def test_duplicate_irqs_deduplicated(self): + """Test duplicate IRQs are deduplicated""" + result, match_requested = tuna_cmd.irqstring_to_list('1,2,1,42,2') + self.assertEqual(sorted(result), [1, 2, 42]) + self.assertTrue(match_requested) + + +class TestSocketstringToList(unittest.TestCase): + """Test cases for socketstring_to_list() function""" + + @classmethod + def setUpClass(cls): + """Load the tuna-cmd module""" + spec.loader.exec_module(tuna_cmd) + + def test_socketstring_exists(self): + """Test that socketstring_to_list function exists""" + # Note: socketstring_to_list doesn't return match_requested tuple + # and requires access to sysfs.cpus() which may not be available + # in test environment. Just verify the function exists. + self.assertTrue(callable(tuna_cmd.socketstring_to_list)) + + +if __name__ == '__main__': + unittest.main() -- 2.54.0