From: John Kacur <jkacur@redhat.com>
To: linux-rt-users@vger.kernel.org
Cc: Clark Williams <williams@redhat.com>
Subject: [PATCH 2/2] tuna: Add testing infrastructure using unittest framework
Date: Thu, 7 May 2026 15:28:58 -0400 [thread overview]
Message-ID: <20260507192858.186090-2-jkacur@redhat.com> (raw)
In-Reply-To: <20260507192858.186090-1-jkacur@redhat.com>
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 <noreply@anthropic.com>
Signed-off-by: John Kacur <jkacur@redhat.com>
---
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
prev parent reply other threads:[~2026-05-07 19:29 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-07 19:28 [PATCH 1/2] tuna: Print warning if setting affinity results in EPERM and continue John Kacur
2026-05-07 19:28 ` 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=20260507192858.186090-2-jkacur@redhat.com \
--to=jkacur@redhat.com \
--cc=linux-rt-users@vger.kernel.org \
--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).