[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