[PATCH 13/36] tuna: Add comprehensive tests for cpuset move command
John Kacur <[email protected]> Fri, 10 Jul 2026 10:14:51 -0400
| Newsgroups | org.kernel.vger.linux-rt-users |
|---|---|
| Message-ID | <[email protected]> |
Adds unit tests and CLI tests for the cpuset move functionality, plus updates test documentation. Test coverage: - TestProcessMovement (3 tests): Unit tests for write_pid() functionality - test_write_pid_self: Moving current process to cpuset - test_write_pid_invalid: Invalid PID handling - test_multiple_pids: Multiple process migration - TestCpusetMoveCLI (6 tests): Integration tests for CLI command - test_move_with_pids: Moving processes with --pids argument - test_move_with_threads: Moving processes with --threads argument - test_move_both_pids_and_threads: Using both arguments together - test_move_nonexistent_cpuset_error: Error handling for missing cpuset - test_move_no_pids_error: Error handling when no PIDs provided - test_move_invalid_pid: Graceful handling of invalid PIDs All tests include proper setup/teardown to ensure cpusets are cleaned up and processes are moved back to root cgroup after testing. Updates tests/README.md to reflect new test count (63 total, up from 57) and documents the new TestCpusetMoveCLI test class. Assisted-by: Claude:claude-sonnet-4-5 Signed-off-by: John Kacur <[email protected]> --- tests/README.md | 36 +++++++------- tests/test_cpuset.py | 76 +++++++++++++++++++++++++++++ tests/test_cpuset_cli.py | 102 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 17 deletions(-) diff --git a/tests/README.md b/tests/README.md index 7f3e53c2c601..ea54d2cee5fd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -9,10 +9,10 @@ This directory contains the test suite for tuna, using Python's `unittest` frame ## Quick Start ```bash -# Run all non-root tests only (23 tests, some cpuset tests skipped) +# Run all non-root tests only (23 tests) make tests -# Run ALL tests (all 57 tests, requires sudo for root-required tests) +# Run ALL tests (all 63 tests, requires sudo for root-required tests) make test-all # Run specific test module @@ -37,7 +37,7 @@ make tests # or make unit-tests -# Using make (ALL tests including root-required tests, 57 tests) +# Using make (ALL tests including root-required tests, 63 tests) make test-all # Using the test runner directly (non-root tests only) @@ -129,7 +129,7 @@ Tests are organized using Python's `unittest` framework. Each test file contains - These tests can run without root as they only check system capabilities - Run: `python3 -m unittest tests.test_cpuset.TestCpusetsInit -v` -#### Root-Required Tests (34 tests) +#### Root-Required Tests (40 tests) - **test_cpuset.py** (22 tests) - Core cpuset module tests - TestCpusetCreation: Creating and destroying cpusets @@ -141,14 +141,15 @@ Tests are organized using Python's `unittest` framework. Each test file contains - Note: TestCpusetsInit (5 tests) runs without root, see non-root section - Run: `sudo python3 -m unittest tests.test_cpuset -v` (runs all 27 tests with root) -- **test_cpuset_cli.py** (12 tests) - CLI command tests +- **test_cpuset_cli.py** (18 tests) - CLI command tests - TestGetNextTunaCpusetName: Auto-naming logic (tuna0, tuna1, gap-filling) - TestCpusetCreateCLI: Create command with auto/custom names and --isolated - TestCpusetListCLI: List command with --pattern, --verbose, --skip-empty - TestCpusetDestroyCLI: Destroy command with patterns and safety options + - TestCpusetMoveCLI: Move command with --pids, --threads, error handling - Run: `sudo python3 -m unittest tests.test_cpuset_cli -v` -**Total: 57 tests** (23 non-root + 34 root-required) +**Total: 63 tests** (23 non-root + 40 root-required) ### Test Requirements: Root vs Non-Root @@ -178,7 +179,7 @@ These tests run without root privileges: - Tests are fast and don't affect the running system - Tests verify both happy path (with mocks) and error path (with real EPERM) -#### Root-Required Tests (34 tests) +#### Root-Required Tests (40 tests) These tests require root to create/manipulate cgroups: @@ -188,10 +189,11 @@ These tests require root to create/manipulate cgroups: - Test task migration between cpusets - All tests clean up created cpusets in tearDown() -2. **Cpuset CLI tests** (12 tests) test command-line interface functions - - Test cpuset create/list/destroy commands +2. **Cpuset CLI tests** (18 tests) test command-line interface functions + - Test cpuset create/list/destroy/move commands - Verify auto-naming logic (tuna0, tuna1, etc.) - Test pattern matching and safety features + - Test process migration with --pids and --threads - All tests clean up created cpusets afterwards **Why root is required:** @@ -312,25 +314,25 @@ test_single_numeric_pid (test_converters.TestThreadstringToList) ... ok ---------------------------------------------------------------------- Ran 23 tests in 0.XXXs -OK (skipped=34) +OK (skipped=40) ``` -### All Tests Including Root-Required (57 tests) +### All Tests Including Root-Required (63 tests) -Running with sudo will run all 57 tests: +Running with sudo will run all 63 tests: ```bash $ sudo python3 -m unittest discover -s tests -p "test_*.py" -v -... (18 non-root tests as above) +... (23 non-root tests as above) test_initialization (test_cpuset.TestCpusetsInit) ... ok test_cpuset_path (test_cpuset.TestCpusetsInit) ... ok ... (27 cpuset module tests) test_first_name_is_tuna0 (test_cpuset_cli.TestGetNextTunaCpusetName) ... ok test_sequential_naming (test_cpuset_cli.TestGetNextTunaCpusetName) ... ok -... (12 cpuset CLI tests) +... (18 cpuset CLI tests) ---------------------------------------------------------------------- -Ran 57 tests in X.XXXs +Ran 63 tests in X.XXXs OK ``` @@ -344,7 +346,7 @@ The test suite includes: - **EPERM tests**: Error handling verification - **CpusetsInit tests**: System capability detection (read-only) -### Integration Tests (34 tests - require root) +### Integration Tests (40 tests - require root) - **Cpuset module tests**: Test interaction with cgroup v2 subsystem - **Cpuset CLI tests**: Test command-line interface integration @@ -371,7 +373,7 @@ python3 -m unittest discover -s tests -p "test_*.py" -v ### Root-Required Tests (Optional for CI) -The cpuset tests (34 tests) can run in CI with special setup: +The cpuset tests (40 tests) can run in CI with special setup: - Requires root access or privileged containers - Requires cgroup v2 support - May need dedicated test runners with appropriate permissions diff --git a/tests/test_cpuset.py b/tests/test_cpuset.py index 6ac4fe0c9c40..ec6ce19028cf 100644 --- a/tests/test_cpuset.py +++ b/tests/test_cpuset.py @@ -579,6 +579,81 @@ class TestCleanupFunctions(unittest.TestCase): self.assertFalse(os.path.exists(parent.cpuset_path)) +class TestProcessMovement(unittest.TestCase): + """Test moving individual processes to cpusets""" + + @unittest.skipUnless(os.geteuid() == 0, "Requires root") + def test_write_pid_self(self): + """Test moving current process to a cpuset""" + # Pre-cleanup + cpuset.cleanup_cpusets('test_write_pid*', force=True, recursive=True) + + cs = cpuset.Cpuset('test_write_pid') + try: + # Configure cpuset + cs.write_memnode('0') + cs.assign_cpus('0-1') + + # Move current process + current_pid = os.getpid() + result = cs.write_pid(current_pid) + self.assertTrue(result, "write_pid should return True on success") + + # Verify it's in the cpuset + tasks = cs.get_tasks() + self.assertIn(str(current_pid), tasks, "Current PID should be in cpuset") + + # Move back to root + root = cpuset.CpusetsInit() + root.write_pid(current_pid) + finally: + # Cleanup + cpuset.destroy_cpuset('test_write_pid', force=True) + + @unittest.skipUnless(os.geteuid() == 0, "Requires root") + def test_write_pid_invalid(self): + """Test write_pid with invalid PID""" + # Pre-cleanup + cpuset.cleanup_cpusets('test_write_invalid*', force=True, recursive=True) + + cs = cpuset.Cpuset('test_write_invalid') + try: + cs.write_memnode('0') + cs.assign_cpus('0-1') + + # Try to move non-existent PID + result = cs.write_pid(999999) + self.assertFalse(result, "write_pid should return False for invalid PID") + finally: + # Cleanup + cpuset.destroy_cpuset('test_write_invalid', force=True) + + @unittest.skipUnless(os.geteuid() == 0, "Requires root") + def test_multiple_pids(self): + """Test moving multiple processes to a cpuset""" + # Pre-cleanup + cpuset.cleanup_cpusets('test_multi_pid*', force=True, recursive=True) + + cs = cpuset.Cpuset('test_multi_pid') + try: + cs.write_memnode('0') + cs.assign_cpus('0-1') + + # Move current process + current_pid = os.getpid() + cs.write_pid(current_pid) + + # Get current tasks + tasks = cs.get_tasks() + self.assertIn(str(current_pid), tasks) + + # Move back to root + root = cpuset.CpusetsInit() + root.write_pid(current_pid) + finally: + cpuset.destroy_cpuset('test_multi_pid', force=True) + + def suite(): """Create test suite""" loader = unittest.TestLoader() @@ -592,6 +667,7 @@ def suite(): suite.addTests(loader.loadTestsFromTestCase(TestTaskMigration)) suite.addTests(loader.loadTestsFromTestCase(TestDiscoveryFunctions)) suite.addTests(loader.loadTestsFromTestCase(TestCleanupFunctions)) + suite.addTests(loader.loadTestsFromTestCase(TestProcessMovement)) return suite diff --git a/tests/test_cpuset_cli.py b/tests/test_cpuset_cli.py index 7f9a4ce9947c..9bec8955d1ad 100644 --- a/tests/test_cpuset_cli.py +++ b/tests/test_cpuset_cli.py @@ -37,6 +37,7 @@ get_next_tuna_cpuset_name = tuna_cmd.get_next_tuna_cpuset_name cpuset_create = tuna_cmd.cpuset_create cpuset_list = tuna_cmd.cpuset_list cpuset_destroy = tuna_cmd.cpuset_destroy +cpuset_move = tuna_cmd.cpuset_move @unittest.skipUnless(os.geteuid() == 0, "Requires root permissions") @@ -314,6 +315,106 @@ class TestCpusetDestroyCLI(unittest.TestCase): self.assertTrue(os.path.exists(cs2.cpuset_path)) [email protected](os.geteuid() == 0, "Requires root permissions") [email protected](cpuset.CpusetsInit().supported, "Requires cgroup v2 support") +class TestCpusetMoveCLI(unittest.TestCase): + """Test 'tuna cpuset move' command""" + + def setUp(self): + """Set up test fixtures""" + self.ci = cpuset.CpusetsInit() + self.created_cpusets = [] + + def tearDown(self): + """Clean up all created cpusets and move test process back to root""" + # Move current process back to root + try: + root = cpuset.CpusetsInit() + root.write_pid(os.getpid()) + except: + pass + + # Clean up cpusets + for name in self.created_cpusets: + try: + cpuset.destroy_cpuset(name, force=True) + except: + pass + + def test_move_with_pids(self): + """Test moving processes with --pids argument""" + # Create a cpuset + cpuset_create(cpu_list=[0, 1], name='test_move_pids', isolated=False) + self.created_cpusets.append('test_move_pids') + + # Move current process using pid_list (string format) + current_pid = os.getpid() + cpuset_move(name='test_move_pids', thread_list=None, pid_list=str(current_pid)) + + # Verify PID is in the cpuset + with open('/sys/fs/cgroup/test_move_pids/cgroup.procs') as f: + pids = f.read().strip().split('\n') + self.assertIn(str(current_pid), pids) + + def test_move_with_threads(self): + """Test moving processes with --threads argument (numeric PIDs)""" + # Create a cpuset + cpuset_create(cpu_list=[0, 1], name='test_move_threads', isolated=False) + self.created_cpusets.append('test_move_threads') + + # Move current process using thread_list (list of integers) + current_pid = os.getpid() + cpuset_move(name='test_move_threads', thread_list=[current_pid], pid_list=None) + + # Verify PID is in the cpuset + with open('/sys/fs/cgroup/test_move_threads/cgroup.procs') as f: + pids = f.read().strip().split('\n') + self.assertIn(str(current_pid), pids) + + def test_move_both_pids_and_threads(self): + """Test moving processes with both --pids and --threads""" + # Create a cpuset + cpuset_create(cpu_list=[0, 1], name='test_move_both', isolated=False) + self.created_cpusets.append('test_move_both') + + # Move current process using both arguments + current_pid = os.getpid() + cpuset_move(name='test_move_both', thread_list=[current_pid], pid_list=str(current_pid)) + + # Verify PID is in the cpuset (should only appear once despite being in both lists) + with open('/sys/fs/cgroup/test_move_both/cgroup.procs') as f: + pids = f.read().strip().split('\n') + self.assertIn(str(current_pid), pids) + + def test_move_nonexistent_cpuset_error(self): + """Test error when trying to move to nonexistent cpuset""" + # Should exit with error (sys.exit(1)) + with self.assertRaises(SystemExit) as cm: + cpuset_move(name='nonexistent_cpuset', thread_list=None, pid_list='1234') + self.assertEqual(cm.exception.code, 1) + + def test_move_no_pids_error(self): + """Test error when no PIDs are provided""" + # Create a cpuset + cpuset_create(cpu_list=[0, 1], name='test_move_nopids', isolated=False) + self.created_cpusets.append('test_move_nopids') + + # Should exit with error when neither thread_list nor pid_list provided + with self.assertRaises(SystemExit) as cm: + cpuset_move(name='test_move_nopids', thread_list=None, pid_list=None) + self.assertEqual(cm.exception.code, 2) + + def test_move_invalid_pid(self): + """Test moving invalid PID (should fail gracefully)""" + # Create a cpuset + cpuset_create(cpu_list=[0, 1], name='test_move_invalid', isolated=False) + self.created_cpusets.append('test_move_invalid') + + # Try to move non-existent PID (should succeed but report 0 moved, 1 failed) + # Since we can't easily capture stdout, just verify it doesn't crash + cpuset_move(name='test_move_invalid', thread_list=[999999], pid_list=None) + + def suite(): """Create test suite""" loader = unittest.TestLoader() @@ -324,6 +425,7 @@ def suite(): suite.addTests(loader.loadTestsFromTestCase(TestCpusetCreateCLI)) suite.addTests(loader.loadTestsFromTestCase(TestCpusetListCLI)) suite.addTests(loader.loadTestsFromTestCase(TestCpusetDestroyCLI)) + suite.addTests(loader.loadTestsFromTestCase(TestCpusetMoveCLI)) return suite -- 2.54.0