[PATCH 28/36] tuna: Add tests for isolate --cpuset functionality

John Kacur <[email protected]> Fri, 10 Jul 2026 10:15:06 -0400
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Add comprehensive test coverage for the new 'tuna isolate --cpuset'
feature that enables cpuset-based CPU isolation.

New test class TestTunaIsolateCpusetCLI includes 8 tests:
- Simple isolated cpuset creation with default/custom names
- Isolated + housekeeping cpuset creation with default/custom names
- Process migration verification to housekeeping cpuset
- Error handling for invalid argument combinations
- Backward compatibility with traditional isolate command

Tests verify:
- Correct cpuset creation with specified CPUs
- Partition types set to 'isolated' for both cpusets
- Process migration from root to housekeeping cpuset
- Error messages for --cpuset-housekeeping without --cpuset
- Traditional 'isolate -c' still works without creating cpusets

Updated Makefile to add 'make test-isolate-cpuset' target for
running these tests independently.

Updated tests/README.md to document the new tests and reflect
the increased test count (116 → 124 total tests).

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: John Kacur <[email protected]>
---
 Makefile                 |   5 +-
 tests/README.md          |  16 +++-
 tests/test_cpuset_cli.py | 190 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 207 insertions(+), 4 deletions(-)

diff --git a/Makefile b/Makefile
index e2e2cbbf9d51..7d3d3f10b6ab 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ cleanlogs:
 .PHONY: clean
 clean: pyclean
 
-.PHONY: tests unit-tests test-eperm test-converters test-cpuset test-cpuset-cli test-all
+.PHONY: tests unit-tests test-eperm test-converters test-cpuset test-cpuset-cli test-isolate-cpuset test-all
 tests: unit-tests
 
 unit-tests:
@@ -39,4 +39,7 @@ test-cpuset:
 test-cpuset-cli:
 	@sudo python3 -m unittest tests.test_cpuset_cli -v
 
+test-isolate-cpuset:
+	@sudo python3 -m unittest tests.test_cpuset_cli.TestTunaIsolateCpusetCLI -v
+
 test-all: test-eperm test-converters test-cpuset test-cpuset-cli
diff --git a/tests/README.md b/tests/README.md
index 6da490b60448..514907db43ee 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -62,12 +62,14 @@ make test-converters
 # Using make (root-required tests)
 make test-cpuset
 make test-cpuset-cli
+make test-isolate-cpuset
 
 # Or using Python unittest directly
 python3 -m unittest tests.test_eperm_handling -v
 python3 -m unittest tests.test_converters -v
 sudo python3 -m unittest tests.test_cpuset -v
 sudo python3 -m unittest tests.test_cpuset_cli -v
+sudo python3 -m unittest tests.test_cpuset_cli.TestTunaIsolateCpusetCLI -v
 ```
 
 ### Specific Test Class
@@ -82,6 +84,7 @@ python3 -m unittest tests.test_converters.TestIrqstringToList -v
 # Root-required tests
 sudo python3 -m unittest tests.test_cpuset.TestCpusetCreation -v
 sudo python3 -m unittest tests.test_cpuset_cli.TestCpusetCreateCLI -v
+sudo python3 -m unittest tests.test_cpuset_cli.TestTunaIsolateCpusetCLI -v
 ```
 
 ### Specific Test Method
@@ -96,6 +99,7 @@ python3 -m unittest tests.test_converters.TestThreadstringToList.test_pattern_wi
 # Root-required tests
 sudo python3 -m unittest tests.test_cpuset.TestCpusetCreation.test_create_cpuset -v
 sudo python3 -m unittest tests.test_cpuset_cli.TestCpusetCreateCLI.test_create_with_auto_naming -v
+sudo python3 -m unittest tests.test_cpuset_cli.TestTunaIsolateCpusetCLI.test_isolate_with_housekeeping_default_names -v
 ```
 
 ## Test Organization
@@ -172,9 +176,13 @@ Tests are organized using Python's `unittest` framework. Each test file contains
   - 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)
+  - TestTunaIsolateCpusetCLI: Integration of --cpuset with tuna isolate command (8 tests)
+    - Tests cpuset-based CPU isolation with isolated and housekeeping cpusets
+    - Verifies partition types, CPU assignments, and process migration
+    - Tests error handling and backward compatibility
   - Run: `sudo python3 -m unittest tests.test_cpuset_cli -v`
 
-**Total: 116 tests** (37 non-root + 79 root-required)
+**Total: 124 tests** (37 non-root + 87 root-required)
 
 ### Test Requirements: Root vs Non-Root
 
@@ -214,7 +222,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 (79 tests)
+#### Root-Required Tests (87 tests)
 
 These tests require root to create/manipulate cgroups:
 
@@ -224,13 +232,15 @@ 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** (40 tests) test command-line interface functions
+2. **Cpuset CLI tests** (48 tests) test command-line interface functions
    - Test cpuset create/list/destroy/move/show/status/modify commands
    - Verify auto-naming logic (tuna0, tuna1, etc.)
    - Test NUMA-aware memory node auto-detection and manual override
    - Test pattern matching and safety features
    - Test process migration with --pids and --threads
    - Test cpuset modification with --add-cpus, --remove-cpus, --memory-nodes, --isolated
+   - Test tuna move --cpuset integration (5 tests)
+   - Test tuna isolate --cpuset integration (8 tests) - cpuset-based CPU isolation
    - All tests clean up created cpusets afterwards
 
 **Why root is required:**
diff --git a/tests/test_cpuset_cli.py b/tests/test_cpuset_cli.py
index ed8995782872..cc51428cc55b 100644
--- a/tests/test_cpuset_cli.py
+++ b/tests/test_cpuset_cli.py
@@ -1006,6 +1006,195 @@ class TestTunaMoveCpusetCLI(unittest.TestCase):
         self.assertIn(str(current_pid), pids)
 
 
[email protected](os.geteuid() == 0, "Requires root permissions")
[email protected](cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
+class TestTunaIsolateCpusetCLI(unittest.TestCase):
+    """Test 'tuna isolate --cpuset' command (cpuset-based CPU isolation)"""
+
+    def setUp(self):
+        """Clean up any test cpusets before each test"""
+        self.test_cpusets = []
+        # Clean up any existing test cpusets
+        try:
+            cpuset.cleanup_cpusets('tuna_*', force=True, recursive=False)
+            cpuset.cleanup_cpusets('my_*', force=True, recursive=False)
+        except:
+            pass
+
+    def tearDown(self):
+        """Clean up test cpusets after each test"""
+        # Clean up by pattern (catches both named and default cpusets)
+        try:
+            cpuset.cleanup_cpusets('tuna_*', force=True, recursive=False)
+            cpuset.cleanup_cpusets('my_*', force=True, recursive=False)
+        except:
+            pass
+
+    def test_isolate_simple_cpuset_default_name(self):
+        """Test simple isolated cpuset with default name"""
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'isolate',
+            '-c', '0-1', '--cpuset'
+        ], capture_output=True, text=True)
+
+        self.assertEqual(result.returncode, 0, f"Command failed: {result.stderr}")
+        self.assertIn('tuna_isolated', result.stdout)
+
+        # Verify cpuset exists and has correct properties
+        cpuset_path = '/sys/fs/cgroup/tuna_isolated'
+        self.assertTrue(os.path.exists(cpuset_path))
+
+        with open(f'{cpuset_path}/cpuset.cpus') as f:
+            cpus = f.read().strip()
+            self.assertIn('0', cpus)
+            self.assertIn('1', cpus)
+
+        with open(f'{cpuset_path}/cpuset.cpus.partition') as f:
+            partition = f.read().strip()
+            self.assertEqual(partition, 'isolated')
+
+        # Verify it's empty (no tasks)
+        with open(f'{cpuset_path}/cgroup.procs') as f:
+            tasks = f.read().strip()
+            self.assertEqual(tasks, '', 'Isolated cpuset should be empty')
+
+    def test_isolate_custom_isolated_name(self):
+        """Test isolated cpuset with custom name"""
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'isolate',
+            '-c', '0-1', '--cpuset', 'my_rt'
+        ], capture_output=True, text=True)
+
+        self.assertEqual(result.returncode, 0, f"Command failed: {result.stderr}")
+        self.assertIn('my_rt', result.stdout)
+
+        # Verify cpuset exists
+        cpuset_path = '/sys/fs/cgroup/my_rt'
+        self.assertTrue(os.path.exists(cpuset_path))
+
+        with open(f'{cpuset_path}/cpuset.cpus') as f:
+            cpus = f.read().strip()
+            self.assertIn('0', cpus)
+
+        with open(f'{cpuset_path}/cpuset.cpus.partition') as f:
+            partition = f.read().strip()
+            self.assertEqual(partition, 'isolated')
+
+    def test_isolate_with_housekeeping_default_names(self):
+        """Test isolated + housekeeping cpusets with default names"""
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'isolate',
+            '-c', '0-1', '--cpuset', '--cpuset-housekeeping', '2-3'
+        ], capture_output=True, text=True)
+
+        self.assertEqual(result.returncode, 0, f"Command failed: {result.stderr}")
+        self.assertIn('tuna_isolated', result.stdout)
+        self.assertIn('tuna_housekeeping', result.stdout)
+
+        # Verify isolated cpuset
+        isolated_path = '/sys/fs/cgroup/tuna_isolated'
+        self.assertTrue(os.path.exists(isolated_path))
+
+        with open(f'{isolated_path}/cpuset.cpus') as f:
+            cpus = f.read().strip()
+            self.assertIn('0', cpus)
+
+        with open(f'{isolated_path}/cpuset.cpus.partition') as f:
+            partition = f.read().strip()
+            self.assertEqual(partition, 'isolated')
+
+        # Verify housekeeping cpuset
+        hk_path = '/sys/fs/cgroup/tuna_housekeeping'
+        self.assertTrue(os.path.exists(hk_path))
+
+        with open(f'{hk_path}/cpuset.cpus') as f:
+            cpus = f.read().strip()
+            self.assertIn('2', cpus)
+            self.assertIn('3', cpus)
+
+        with open(f'{hk_path}/cpuset.cpus.partition') as f:
+            partition = f.read().strip()
+            self.assertEqual(partition, 'isolated')
+
+        # Verify processes were migrated to housekeeping
+        with open(f'{hk_path}/cgroup.procs') as f:
+            tasks = f.read().strip().split('\n')
+            # Should have tasks migrated (at least some)
+            self.assertGreater(len(tasks), 0, 'Housekeeping cpuset should have migrated tasks')
+            # Filter out empty strings
+            tasks = [t for t in tasks if t]
+            self.assertGreater(len(tasks), 0)
+
+    def test_isolate_custom_names_both_cpusets(self):
+        """Test isolated + housekeeping with custom names for both"""
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'isolate',
+            '-c', '0-1', '--cpuset', 'my_rt',
+            '--cpuset-housekeeping', 'my_hk', '2-3'
+        ], capture_output=True, text=True)
+
+        self.assertEqual(result.returncode, 0, f"Command failed: {result.stderr}")
+        self.assertIn('my_rt', result.stdout)
+        self.assertIn('my_hk', result.stdout)
+
+        # Verify both cpusets exist
+        self.assertTrue(os.path.exists('/sys/fs/cgroup/my_rt'))
+        self.assertTrue(os.path.exists('/sys/fs/cgroup/my_hk'))
+
+        # Verify my_rt has correct CPUs
+        with open('/sys/fs/cgroup/my_rt/cpuset.cpus') as f:
+            cpus = f.read().strip()
+            self.assertIn('0', cpus)
+
+        # Verify my_hk has correct CPUs
+        with open('/sys/fs/cgroup/my_hk/cpuset.cpus') as f:
+            cpus = f.read().strip()
+            self.assertIn('2', cpus)
+            self.assertIn('3', cpus)
+
+        # Both should be isolated partitions
+        with open('/sys/fs/cgroup/my_rt/cpuset.cpus.partition') as f:
+            self.assertEqual(f.read().strip(), 'isolated')
+
+        with open('/sys/fs/cgroup/my_hk/cpuset.cpus.partition') as f:
+            self.assertEqual(f.read().strip(), 'isolated')
+
+    def test_isolate_housekeeping_without_cpuset_error(self):
+        """Test error when using --cpuset-housekeeping without --cpuset"""
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'isolate',
+            '-c', '0-1', '--cpuset-housekeeping', '2-3'
+        ], capture_output=True, text=True)
+
+        self.assertNotEqual(result.returncode, 0)
+        self.assertIn('requires --cpuset', result.stderr)
+
+    def test_isolate_housekeeping_wrong_arg_count_error(self):
+        """Test error when --cpuset-housekeeping has wrong number of arguments"""
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'isolate',
+            '-c', '0-1', '--cpuset',
+            '--cpuset-housekeeping', 'foo', 'bar', 'baz'
+        ], capture_output=True, text=True)
+
+        self.assertNotEqual(result.returncode, 0)
+        self.assertIn('1 or 2 arguments', result.stderr)
+
+    def test_isolate_traditional_without_cpuset(self):
+        """Test traditional isolate without --cpuset (backward compatibility)"""
+        result = subprocess.run([
+            sys.executable, './tuna-cmd.py', 'isolate',
+            '-c', '0-1'
+        ], capture_output=True, text=True)
+
+        # Should succeed
+        self.assertEqual(result.returncode, 0)
+
+        # Verify no cpusets were created
+        self.assertFalse(os.path.exists('/sys/fs/cgroup/tuna_isolated'))
+        self.assertFalse(os.path.exists('/sys/fs/cgroup/tuna_housekeeping'))
+
+
 def suite():
     """Create test suite"""
     loader = unittest.TestLoader()
@@ -1021,6 +1210,7 @@ def suite():
     suite.addTests(loader.loadTestsFromTestCase(TestCpusetStatusCLI))
     suite.addTests(loader.loadTestsFromTestCase(TestCpusetModifyCLI))
     suite.addTests(loader.loadTestsFromTestCase(TestTunaMoveCpusetCLI))
+    suite.addTests(loader.loadTestsFromTestCase(TestTunaIsolateCpusetCLI))
 
     return suite
 
-- 
2.54.0