[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