[PATCH 2/2] tuna: Add testing infrastructure using unittest framework

John Kacur <[email protected]> Thu, 7 May 2026 15:28:58 -0400
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Create a clean testing infrastructure for tuna using Python's unittest
framework. This provides a foundation for unit tests and future test
expansion.

Structure:
- tests/ directory with unittest-based tests
- tests/run_tests.sh runner script
- Makefile targets for easy test execution
- Comprehensive README.md with examples and documentation

Testing Infrastructure:
- Runner script (run_tests.sh) that uses unittest discovery
  * Automatically finds all test_*.py files
  * Provides clean output
  * Changes to repo root before running

- Makefile targets:
  * make tests / make unit-tests - Run all unit tests
  * make test-eperm - Run specific EPERM test

- Multiple execution methods supported:
  * Via make (recommended for users)
  * Via runner script (./tests/run_tests.sh)
  * Via unittest directly (python3 -m unittest discover)

Initial Test:
- test_eperm_handling.py with 6 test methods
- Validates EPERM error handling in isolate_cpus()
- Tests verify the fix from commit 625edd878154
- All tests run without root privileges or special configuration

The infrastructure is designed for easy expansion - simply add new
test_*.py files and they're automatically discovered and run.

Assisted-by: Claude Sonnet 4.5 <[email protected]>
Signed-off-by: John Kacur <[email protected]>
---
 Makefile                     |   9 +++
 tests/README.md              | 147 +++++++++++++++++++++++++++++++++++
 tests/run_tests.sh           |  25 ++++++
 tests/test_eperm_handling.py |  87 +++++++++++++++++++++
 4 files changed, 268 insertions(+)
 create mode 100644 tests/README.md
 create mode 100755 tests/run_tests.sh
 create mode 100644 tests/test_eperm_handling.py

diff --git a/Makefile b/Makefile
index a55821a8f908..f331f4715d1c 100644
--- a/Makefile
+++ b/Makefile
@@ -20,3 +20,12 @@ cleanlogs:
 
 .PHONY: clean
 clean: pyclean
+
+.PHONY: tests unit-tests
+tests: unit-tests
+
+unit-tests:
+	@./tests/run_tests.sh
+
+test-eperm:
+	@python3 -m unittest tests.test_eperm_handling -v
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 000000000000..aceb38d6f8f5
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,147 @@
+# Tuna Test Suite
+
+This directory contains the test suite for tuna, using Python's `unittest` framework.
+
+## Running Tests
+
+### All Tests
+
+Run all unit tests using any of these methods:
+
+```bash
+# Using make (recommended)
+make tests
+# or
+make unit-tests
+
+# Using the test runner directly
+./tests/run_tests.sh
+
+# Using Python unittest directly
+python3 -m unittest discover -s tests -p "test_*.py" -v
+```
+
+### Specific Test File
+
+Run a specific test file:
+
+```bash
+# Using make
+make test-eperm
+
+# Using Python unittest
+python3 -m unittest tests.test_eperm_handling -v
+```
+
+### Specific Test Method
+
+Run a single test method:
+
+```bash
+python3 -m unittest tests.test_eperm_handling.TestEPERMHandling.test_eperm_constant_exists -v
+```
+
+## Test Organization
+
+Tests are organized using Python's `unittest` framework. Each test file contains:
+- A test class that inherits from `unittest.TestCase`
+- Individual test methods (prefixed with `test_`)
+- Setup/teardown methods if needed
+
+### Current Tests
+
+- **test_eperm_handling.py** - Tests for EPERM error handling in `isolate_cpus()`
+  - Verifies that Permission Denied errors are handled gracefully
+  - Ensures tuna continues processing when it can't set affinity on protected processes
+
+## Writing New Tests
+
+### Test File Structure
+
+Create a new test file following this pattern:
+
+```python
+#!/usr/bin/env python3
+"""
+Description of what this test module tests.
+"""
+
+import unittest
+import os
+import sys
+
+
+class TestSomething(unittest.TestCase):
+    """Test cases for something in tuna"""
+
+    @classmethod
+    def setUpClass(cls):
+        """Set up test fixtures (runs once before all tests)"""
+        # Common setup for all tests in this class
+        pass
+
+    def setUp(self):
+        """Set up for each test (runs before each test method)"""
+        # Per-test setup
+        pass
+
+    def test_something(self):
+        """Test that something works correctly"""
+        # Your test code here
+        self.assertEqual(expected, actual)
+        self.assertTrue(condition)
+        self.assertIn(item, collection)
+
+    def tearDown(self):
+        """Clean up after each test (runs after each test method)"""
+        # Per-test cleanup
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()
+```
+
+### Naming Conventions
+
+- Test files: `test_*.py` (e.g., `test_affinity.py`)
+- Test classes: `Test*` (e.g., `TestAffinityFunctions`)
+- Test methods: `test_*` (e.g., `test_set_affinity_validates_input`)
+
+### Common Assertions
+
+```python
+self.assertEqual(a, b)           # a == b
+self.assertNotEqual(a, b)        # a != b
+self.assertTrue(x)               # bool(x) is True
+self.assertFalse(x)              # bool(x) is False
+self.assertIn(a, b)              # a in b
+self.assertNotIn(a, b)           # a not in b
+self.assertIsNone(x)             # x is None
+self.assertIsNotNone(x)          # x is not None
+self.assertRaises(Exception, fn) # fn() raises Exception
+```
+
+## Requirements
+
+- Python 3.6 or later
+- No additional packages required (uses Python standard library)
+
+## Test Types
+
+Currently all tests are **unit tests** that verify code behavior without requiring:
+- Root privileges
+- Special system configuration
+- External dependencies
+
+Future test categories might include:
+- **Integration tests** - Test interaction between tuna components
+- **System tests** - Tests requiring actual CPU affinity operations (need root)
+- **Regression tests** - Tests for specific bug fixes
+
+## Continuous Integration
+
+The test suite is designed to run in CI/CD environments:
+- All tests run without root privileges
+- No special system configuration required
+- Exit code 0 on success, non-zero on failure
diff --git a/tests/run_tests.sh b/tests/run_tests.sh
new file mode 100755
index 000000000000..da048a69d5d1
--- /dev/null
+++ b/tests/run_tests.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+#   Copyright (C) 2026 John Kacur
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# run_tests.sh - Run tuna unit tests
+#
+# This script runs all unit tests in the tests/ directory using Python's
+# unittest framework.
+
+set -e
+
+# Change to repository root
+cd "$(dirname "$0")/.."
+
+echo "Running tuna unit tests..."
+echo ""
+
+# Run unittest discovery
+# -s tests: start directory
+# -p "test_*.py": pattern for test files
+# -v: verbose output
+python3 -m unittest discover -s tests -p "test_*.py" -v
+
+echo ""
+echo "All tests passed!"
diff --git a/tests/test_eperm_handling.py b/tests/test_eperm_handling.py
new file mode 100644
index 000000000000..c94a3ad6a0b4
--- /dev/null
+++ b/tests/test_eperm_handling.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+#   Copyright (C) 2026 John Kacur
+# SPDX-License-Identifier: GPL-2.0-only
+"""
+Unit tests for EPERM error handling in isolate_cpus function.
+
+Tests verify that tuna properly handles Permission Denied errors when
+attempting to set CPU affinity on processes like PID 1 (systemd/init).
+"""
+
+import unittest
+import os
+import sys
+
+
+class TestEPERMHandling(unittest.TestCase):
+    """Test cases for EPERM error handling in isolate_cpus"""
+
+    @classmethod
+    def setUpClass(cls):
+        """Set up test fixtures - read tuna.py once for all tests"""
+        tuna_path = os.path.join(os.path.dirname(__file__), '..', 'tuna', 'tuna.py')
+        with open(tuna_path, 'r') as f:
+            cls.tuna_content = f.read()
+
+        # Find isolate_cpus function bounds
+        cls.isolate_cpus_start = cls.tuna_content.find('def isolate_cpus(')
+        cls.assertNotEqual(cls.isolate_cpus_start, -1, "Could not find isolate_cpus function")
+
+        # Find next function to get the bounds
+        next_def = cls.tuna_content.find('\ndef ', cls.isolate_cpus_start + 1)
+        if next_def == -1:
+            next_def = len(cls.tuna_content)
+
+        cls.isolate_cpus_code = cls.tuna_content[cls.isolate_cpus_start:next_def]
+
+    def test_eperm_constant_exists(self):
+        """Test that errno.EPERM is referenced in the code"""
+        self.assertIn('errno.EPERM', self.tuna_content,
+                      "errno.EPERM constant not found in tuna.py")
+
+    def test_warning_message_exists(self):
+        """Test that warning message exists for unable to isolate"""
+        self.assertIn('Warning: Unable to isolate pid', self.tuna_content,
+                      "Warning message not found in tuna.py")
+
+    def test_eperm_in_isolate_cpus_function(self):
+        """Test that EPERM handling is in the isolate_cpus function"""
+        self.assertIn('errno.EPERM', self.isolate_cpus_code,
+                      "EPERM handling not found in isolate_cpus function")
+
+    def test_both_ebusy_and_eperm_handled(self):
+        """Test that both EBUSY and EPERM errors are handled"""
+        self.assertIn('errno.EBUSY', self.isolate_cpus_code,
+                      "EBUSY handling not found in isolate_cpus function")
+        self.assertIn('errno.EPERM', self.isolate_cpus_code,
+                      "EPERM handling not found in isolate_cpus function")
+
+    def test_eperm_handler_calls_continue(self):
+        """Test that EPERM handler calls continue to keep processing"""
+        eperm_block_start = self.isolate_cpus_code.find('if err.args[0] == errno.EPERM')
+        self.assertNotEqual(eperm_block_start, -1,
+                           "EPERM handling block not found")
+
+        # Check that continue is called within the next ~200 chars
+        eperm_block = self.isolate_cpus_code[eperm_block_start:eperm_block_start + 200]
+        self.assertIn('continue', eperm_block,
+                     "EPERM handler should call 'continue' to avoid crash")
+
+    def test_eperm_handler_structure(self):
+        """Test that EPERM handler has correct structure with comm extraction"""
+        eperm_block_start = self.isolate_cpus_code.find('if err.args[0] == errno.EPERM')
+        self.assertNotEqual(eperm_block_start, -1, "EPERM handling block not found")
+
+        eperm_block = self.isolate_cpus_code[eperm_block_start:eperm_block_start + 250]
+
+        # Should extract comm for the warning message
+        self.assertIn('comm = ps[pid].stat["comm"]', eperm_block,
+                     "EPERM handler should extract process name")
+
+        # Should print warning with pid and comm
+        self.assertIn('print(f\'Warning: Unable to isolate pid {pid} [{comm}]\')', eperm_block,
+                     "EPERM handler should print warning with pid and comm")
+
+
+if __name__ == '__main__':
+    unittest.main()
-- 
2.54.0