[PATCH 26/36] tuna: Add tests for tuna move --cpuset command

John Kacur <[email protected]> Fri, 10 Jul 2026 10:15:04 -0400
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Add comprehensive test coverage for the new --cpuset option in the
tuna move command. Tests verify basic functionality, error handling,
and process blocklist protection when moving entities to cpusets.

Test coverage:
- test_move_threads_to_cpuset: Basic thread movement to cpuset
- test_move_threads_by_pattern_to_cpuset: Pattern-based thread selection
- test_move_nonexistent_cpuset_error: Error handling for missing cpusets
- test_move_blocklisted_process_to_cpuset: Blocklist protection (PID 1)
- test_move_mixed_valid_and_invalid_pids: Mixed PID validation

All tests require root permissions and cgroup v2 support. Tests follow
existing patterns in TestCpusetMoveCLI, using subprocess.run() to invoke
tuna-cmd.py and verify exit codes, stdout, and actual process placement
in cpusets.

Updates tests/README.md to reflect new test count: 116 total tests
(37 non-root + 79 root-required), up from 111 tests.

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: John Kacur <[email protected]>
---
 tests/README.md          |  26 ++++----
 tests/test_cpuset_cli.py | 131 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 145 insertions(+), 12 deletions(-)

diff --git a/tests/README.md b/tests/README.md
index 4c3ecaca2563..6da490b60448 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -12,7 +12,7 @@ This directory contains the test suite for tuna, using Python's `unittest` frame
 # Run all non-root tests only (37 tests)
 make tests
 
-# Run ALL tests (all 111 tests, requires sudo for root-required tests)
+# Run ALL tests (all 116 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, 111 tests)
+# Using make (ALL tests including root-required tests, 116 tests)
 make test-all
 
 # Using the test runner directly (non-root tests only)
@@ -144,7 +144,7 @@ Tests are organized using Python's `unittest` framework. Each test file contains
   - Validates smart NUMA-aware memory node assignment for cpusets
   - Run: `python3 -m unittest tests.test_cpuset.TestNumaDetection -v`
 
-#### Root-Required Tests (74 tests)
+#### Root-Required Tests (79 tests)
 
 - **test_process_blocklist.py** (2 tests) - Process blocklist root tests
   - Tests actual blocking of systemd from being moved to cpusets
@@ -162,7 +162,7 @@ Tests are organized using Python's `unittest` framework. Each test file contains
   - Note: TestCpusetsInit (5 tests) and TestNumaDetection (6 tests) run without root, see non-root section
   - Run: `sudo python3 -m unittest tests.test_cpuset -v` (runs all 43 tests with root)
 
-- **test_cpuset_cli.py** (40 tests) - CLI command tests
+- **test_cpuset_cli.py** (45 tests) - CLI command tests
   - TestGetNextTunaCpusetName: Auto-naming logic (tuna0, tuna1, gap-filling)
   - TestCpusetCreateCLI: Create command with auto/custom names, --isolated, and NUMA auto-detection/override
   - TestCpusetListCLI: List command with --pattern, --verbose, --skip-empty
@@ -171,9 +171,10 @@ Tests are organized using Python's `unittest` framework. Each test file contains
   - TestCpusetShowCLI: Show command for detailed cpuset information
   - TestCpusetStatusCLI: Status command for system-wide cpuset overview
   - TestCpusetModifyCLI: Modify command with --add-cpus, --remove-cpus, --memory-nodes, --isolated/--no-isolated
+  - TestTunaMoveCpusetCLI: Integration of --cpuset with tuna move command (5 tests)
   - Run: `sudo python3 -m unittest tests.test_cpuset_cli -v`
 
-**Total: 111 tests** (37 non-root + 74 root-required)
+**Total: 116 tests** (37 non-root + 79 root-required)
 
 ### Test Requirements: Root vs Non-Root
 
@@ -213,7 +214,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 (74 tests)
+#### Root-Required Tests (79 tests)
 
 These tests require root to create/manipulate cgroups:
 
@@ -356,12 +357,12 @@ test_numa_node_int (test_cpuset.TestNumaDetection) ... ok
 ----------------------------------------------------------------------
 Ran 37 tests in 0.XXXs
 
-OK (skipped=74)
+OK (skipped=79)
 ```
 
-### All Tests Including Root-Required (111 tests)
+### All Tests Including Root-Required (116 tests)
 
-Running with sudo will run all 111 tests:
+Running with sudo will run all 116 tests:
 
 ```bash
 $ sudo python3 -m unittest discover -s tests -p "test_*.py" -v
@@ -371,10 +372,10 @@ test_cpuset_path (test_cpuset.TestCpusetsInit) ... ok
 ... (32 cpuset module tests)
 test_first_name_is_tuna0 (test_cpuset_cli.TestGetNextTunaCpusetName) ... ok
 test_sequential_naming (test_cpuset_cli.TestGetNextTunaCpusetName) ... ok
-... (38 cpuset CLI tests)
+... (43 cpuset CLI tests)
 
 ----------------------------------------------------------------------
-Ran 111 tests in X.XXXs
+Ran 116 tests in X.XXXs
 
 OK
 ```
@@ -389,7 +390,8 @@ The test suite includes:
 - **CpusetsInit tests**: System capability detection (read-only)
 - **NUMA detection tests**: NUMA topology discovery and memory node assignment (read-only)
 
-### Integration Tests (54 tests - require root)
+### Integration Tests (79 tests - require root)
+- **Process blocklist tests**: Test blocklist protection (2 root-required tests)
 - **Cpuset module tests**: Test interaction with cgroup v2 subsystem
 - **Cpuset CLI tests**: Test command-line interface integration
 
diff --git a/tests/test_cpuset_cli.py b/tests/test_cpuset_cli.py
index fc647ad93c97..ed8995782872 100644
--- a/tests/test_cpuset_cli.py
+++ b/tests/test_cpuset_cli.py
@@ -876,6 +876,136 @@ class TestCpusetModifyCLI(unittest.TestCase):
         self.assertTrue(result.stderr)  # Should have error output
 
 
[email protected](os.geteuid() == 0, "Requires root permissions")
[email protected](cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
+class TestTunaMoveCpusetCLI(unittest.TestCase):
+    """Test 'tuna move --cpuset' command (moves entities to cpusets)"""
+
+    def setUp(self):
+        """Create test cpusets"""
+        self.test_cpusets = []
+
+    def tearDown(self):
+        """Clean up test 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 cs_name in self.test_cpusets:
+            try:
+                cpuset.destroy_cpuset(cs_name, force=True)
+            except:
+                pass
+
+    def test_move_threads_to_cpuset(self):
+        """Test moving threads to cpuset using tuna move --cpuset"""
+        # Create test cpuset
+        cs = cpuset.Cpuset('test_tuna_move')
+        cs.write_memnode('0')
+        cs.assign_cpus('0-1')
+        self.test_cpusets.append('test_tuna_move')
+
+        # Move current process using tuna move --cpuset
+        current_pid = os.getpid()
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'move',
+            '--cpuset', 'test_tuna_move', '-t', str(current_pid)
+        ], capture_output=True, text=True)
+
+        self.assertEqual(result.returncode, 0)
+        self.assertIn('Moved 1 task(s)', result.stdout)
+
+        # Verify PID is in the cpuset
+        with open('/sys/fs/cgroup/test_tuna_move/cgroup.procs') as f:
+            pids = f.read().strip().split('\n')
+        self.assertIn(str(current_pid), pids)
+
+    def test_move_threads_by_pattern_to_cpuset(self):
+        """Test moving threads by pattern to cpuset"""
+        # Create test cpuset
+        cs = cpuset.Cpuset('test_tuna_move_pattern')
+        cs.write_memnode('0')
+        cs.assign_cpus('0-1')
+        self.test_cpusets.append('test_tuna_move_pattern')
+
+        # Move current process (using PID as pattern)
+        current_pid = os.getpid()
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'move',
+            '--cpuset', 'test_tuna_move_pattern', '-t', str(current_pid)
+        ], capture_output=True, text=True)
+
+        self.assertEqual(result.returncode, 0)
+        self.assertIn('Moved', result.stdout)
+
+        # Verify it moved
+        with open('/sys/fs/cgroup/test_tuna_move_pattern/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 moving to nonexistent cpuset"""
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'move',
+            '--cpuset', 'nonexistent_cpuset', '-t', '1'
+        ], capture_output=True, text=True)
+
+        self.assertNotEqual(result.returncode, 0)
+        self.assertIn('does not exist', result.stderr)
+
+    def test_move_blocklisted_process_to_cpuset(self):
+        """Test that blocklisted processes cannot be moved via tuna move --cpuset"""
+        # Create test cpuset
+        cs = cpuset.Cpuset('test_blocklist_move')
+        cs.write_memnode('0')
+        cs.assign_cpus('0-1')
+        self.test_cpusets.append('test_blocklist_move')
+
+        # Try to move PID 1 (systemd) - should be blocked
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'move',
+            '--cpuset', 'test_blocklist_move', '-t', '1'
+        ], capture_output=True, text=True)
+
+        # Should complete but with 0 successful moves
+        self.assertEqual(result.returncode, 0)
+        self.assertIn('0 task(s)', result.stdout)
+
+        # Verify PID 1 is NOT in the cpuset
+        with open('/sys/fs/cgroup/test_blocklist_move/cgroup.procs') as f:
+            pids = f.read().strip().split('\n')
+        self.assertNotIn('1', pids)
+
+    def test_move_mixed_valid_and_invalid_pids(self):
+        """Test moving mix of valid and invalid PIDs"""
+        # Create test cpuset
+        cs = cpuset.Cpuset('test_mixed_move')
+        cs.write_memnode('0')
+        cs.assign_cpus('0-1')
+        self.test_cpusets.append('test_mixed_move')
+
+        # Move our PID (valid) and PID 999999 (likely invalid)
+        current_pid = os.getpid()
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'move',
+            '--cpuset', 'test_mixed_move', '-t', f'{current_pid},999999'
+        ], capture_output=True, text=True)
+
+        self.assertEqual(result.returncode, 0)
+        # Should show some successes and some failures
+        self.assertIn('Moved', result.stdout)
+        self.assertIn('failed', result.stdout)
+
+        # Our PID should be in the cpuset
+        with open('/sys/fs/cgroup/test_mixed_move/cgroup.procs') as f:
+            pids = f.read().strip().split('\n')
+        self.assertIn(str(current_pid), pids)
+
+
 def suite():
     """Create test suite"""
     loader = unittest.TestLoader()
@@ -890,6 +1020,7 @@ def suite():
     suite.addTests(loader.loadTestsFromTestCase(TestCpusetShowCLI))
     suite.addTests(loader.loadTestsFromTestCase(TestCpusetStatusCLI))
     suite.addTests(loader.loadTestsFromTestCase(TestCpusetModifyCLI))
+    suite.addTests(loader.loadTestsFromTestCase(TestTunaMoveCpusetCLI))
 
     return suite
 
-- 
2.54.0