[PATCH 31/36] tuna: Add tests for show_threads --cpuset filter

John Kacur <[email protected]> Fri, 10 Jul 2026 10:15:09 -0400
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Add comprehensive test suite for the new --cpuset filter option in
show_threads command. Tests verify correct filtering behavior across
various scenarios and edge cases.

Test coverage includes:
- Basic filtering to show only processes in specified cpuset
- Filtering with non-existent cpuset (shows nothing)
- Exclusion of processes from other cpusets
- Combining --cpuset with -G (cgroup display)
- Combining --cpuset with -t (thread list)
- Filtering on empty cpusets

The test file includes 7 tests total:
- 1 non-root test: Verifies --cpuset appears in help output
- 6 root-required tests: Create temporary cpusets and processes to test
  filtering behavior, then clean up

Also updates:
- Makefile: Add test-show-threads-cpuset target and include in test-all
- tests/README.md: Update test counts (131 total: 38 non-root + 93 root)
  and document the new test suite

All tests pass successfully and integrate with the existing test
infrastructure using Python's unittest framework.

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: John Kacur <[email protected]>
---
 Makefile                          |   7 +-
 tests/README.md                   |  76 ++++++---
 tests/test_show_threads_cpuset.py | 245 ++++++++++++++++++++++++++++++
 3 files changed, 304 insertions(+), 24 deletions(-)
 create mode 100755 tests/test_show_threads_cpuset.py

diff --git a/Makefile b/Makefile
index 7d3d3f10b6ab..9e3ffe81bda5 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-isolate-cpuset test-all
+.PHONY: tests unit-tests test-eperm test-converters test-cpuset test-cpuset-cli test-isolate-cpuset test-show-threads-cpuset test-all
 tests: unit-tests
 
 unit-tests:
@@ -42,4 +42,7 @@ test-cpuset-cli:
 test-isolate-cpuset:
 	@sudo python3 -m unittest tests.test_cpuset_cli.TestTunaIsolateCpusetCLI -v
 
-test-all: test-eperm test-converters test-cpuset test-cpuset-cli
+test-show-threads-cpuset:
+	@sudo python3 -m unittest tests.test_show_threads_cpuset -v
+
+test-all: test-eperm test-converters test-cpuset test-cpuset-cli test-show-threads-cpuset
diff --git a/tests/README.md b/tests/README.md
index 514907db43ee..f9908326d6f3 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 (37 tests)
+# Run all non-root tests only (38 tests)
 make tests
 
-# Run ALL tests (all 116 tests, requires sudo for root-required tests)
+# Run ALL tests (all 131 tests, requires sudo for root-required tests)
 make test-all
 
 # Run specific test module
@@ -32,12 +32,12 @@ sudo python3 -m unittest tests.test_cpuset -v
 Run all unit tests using any of these methods:
 
 ```bash
-# Using make (non-root tests only, 37 tests)
+# Using make (non-root tests only, 38 tests)
 make tests
 # or
 make unit-tests
 
-# Using make (ALL tests including root-required tests, 116 tests)
+# Using make (ALL tests including root-required tests, 131 tests)
 make test-all
 
 # Using the test runner directly (non-root tests only)
@@ -63,6 +63,7 @@ make test-converters
 make test-cpuset
 make test-cpuset-cli
 make test-isolate-cpuset
+make test-show-threads-cpuset
 
 # Or using Python unittest directly
 python3 -m unittest tests.test_eperm_handling -v
@@ -70,6 +71,7 @@ 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
+sudo python3 -m unittest tests.test_show_threads_cpuset -v
 ```
 
 ### Specific Test Class
@@ -111,7 +113,7 @@ Tests are organized using Python's `unittest` framework. Each test file contains
 
 ### Current Tests
 
-#### Non-Root Tests (37 tests)
+#### Non-Root Tests (38 tests)
 
 - **test_eperm_handling.py** (6 tests) - Tests for EPERM error handling
   - Verifies that Permission Denied errors are handled gracefully
@@ -148,7 +150,12 @@ 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 (79 tests)
+- **test_show_threads_cpuset.py::TestShowThreadsCpusetFilterNonRoot** (1 test) - Help text verification
+  - Tests that --cpuset option appears in show_threads --help
+  - Can run without root as it only checks help output
+  - Run: `python3 -m unittest tests.test_show_threads_cpuset.TestShowThreadsCpusetFilterNonRoot -v`
+
+#### Root-Required Tests (93 tests)
 
 - **test_process_blocklist.py** (2 tests) - Process blocklist root tests
   - Tests actual blocking of systemd from being moved to cpusets
@@ -182,13 +189,23 @@ Tests are organized using Python's `unittest` framework. Each test file contains
     - Tests error handling and backward compatibility
   - Run: `sudo python3 -m unittest tests.test_cpuset_cli -v`
 
-**Total: 124 tests** (37 non-root + 87 root-required)
+- **test_show_threads_cpuset.py** (6 tests) - show_threads --cpuset filter tests
+  - TestShowThreadsCpusetFilter: Tests filtering processes by cpuset membership
+    - test_filter_shows_only_cpuset_processes: Verifies basic filtering works
+    - test_filter_with_nonexistent_cpuset: Tests non-existent cpuset shows nothing
+    - test_filter_excludes_other_cpuset_processes: Tests exclusion of other cpusets
+    - test_filter_with_cgroups_option: Tests combining --cpuset with -G
+    - test_filter_with_thread_list: Tests combining --cpuset with -t
+    - test_filter_empty_cpuset: Tests filtering on empty cpuset
+  - Run: `sudo python3 -m unittest tests.test_show_threads_cpuset -v`
+
+**Total: 131 tests** (38 non-root + 93 root-required)
 
 ### Test Requirements: Root vs Non-Root
 
 The test suite is split between tests that require root and those that don't:
 
-#### Non-Root Tests (37 tests)
+#### Non-Root Tests (38 tests)
 
 These tests run without root privileges:
 
@@ -216,13 +233,17 @@ These tests run without root privileges:
    - Test smart memory node assignment for CPU lists
    - Read-only operations that work on any system (single or multi-node)
 
+6. **show_threads --cpuset help test** verifies CLI documentation
+   - Tests that --cpuset option appears in help text
+   - Read-only operation checking command-line interface
+
 **Benefits:**
 - Developers can run basic tests without `sudo`
 - Tests run in CI/CD environments without elevated 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 (87 tests)
+#### Root-Required Tests (93 tests)
 
 These tests require root to create/manipulate cgroups:
 
@@ -232,7 +253,7 @@ 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** (48 tests) test command-line interface functions
+2. **Cpuset CLI tests** (45 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
@@ -243,6 +264,14 @@ These tests require root to create/manipulate cgroups:
    - Test tuna isolate --cpuset integration (8 tests) - cpuset-based CPU isolation
    - All tests clean up created cpusets afterwards
 
+3. **show_threads --cpuset filter tests** (6 tests) test process filtering by cpuset
+   - Test basic filtering to show only processes in a specific cpuset
+   - Test filtering with non-existent cpusets (shows nothing)
+   - Test exclusion of processes from other cpusets
+   - Test combining --cpuset with other options (-G, -t)
+   - Test filtering on empty cpusets
+   - All tests create temporary cpusets and processes, then clean up
+
 **Why root is required:**
 - Creating cgroups requires write access to /sys/fs/cgroup
 - Migrating processes between cgroups requires CAP_SYS_ADMIN
@@ -332,7 +361,7 @@ self.assertRaises(Exception, fn) # fn() raises Exception
 
 ## Expected Output
 
-### Non-Root Tests Only (37 tests)
+### Non-Root Tests Only (38 tests)
 
 Running without sudo will run only the non-root tests:
 
@@ -365,27 +394,28 @@ test_numa_node_int (test_cpuset.TestNumaDetection) ... ok
 ... (most cpuset tests skipped: "Requires root permissions")
 
 ----------------------------------------------------------------------
-Ran 37 tests in 0.XXXs
+Ran 38 tests in 0.XXXs
 
-OK (skipped=79)
+OK (skipped=93)
 ```
 
-### All Tests Including Root-Required (116 tests)
+### All Tests Including Root-Required (131 tests)
 
-Running with sudo will run all 116 tests:
+Running with sudo will run all 131 tests:
 
 ```bash
 $ sudo python3 -m unittest discover -s tests -p "test_*.py" -v
-... (37 non-root tests as above)
+... (38 non-root tests as above)
 test_initialization (test_cpuset.TestCpusetsInit) ... ok
 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
-... (43 cpuset CLI tests)
+... (45 cpuset CLI tests)
+... (6 show_threads --cpuset tests)
 
 ----------------------------------------------------------------------
-Ran 116 tests in X.XXXs
+Ran 131 tests in X.XXXs
 
 OK
 ```
@@ -394,16 +424,18 @@ OK
 
 The test suite includes:
 
-### Unit Tests (37 tests - no root required)
+### Unit Tests (38 tests - no root required)
 - **Converter tests**: Pure logic testing with mocks
 - **EPERM tests**: Error handling verification
 - **CpusetsInit tests**: System capability detection (read-only)
 - **NUMA detection tests**: NUMA topology discovery and memory node assignment (read-only)
+- **show_threads --cpuset help test**: CLI documentation verification (read-only)
 
-### Integration Tests (79 tests - require root)
+### Integration Tests (93 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
+- **show_threads --cpuset tests**: Test process filtering by cpuset membership
 
 Future test categories might include:
 - **System tests** - Full end-to-end tests requiring CPU affinity operations
@@ -414,7 +446,7 @@ Future test categories might include:
 
 ### Non-Root Tests (Recommended for CI)
 
-The non-root tests (37 tests) are designed to run in CI/CD environments:
+The non-root tests (38 tests) are designed to run in CI/CD environments:
 - Run without root privileges
 - No special system configuration required
 - Exit code 0 on success, non-zero on failure
@@ -428,7 +460,7 @@ python3 -m unittest discover -s tests -p "test_*.py" -v
 
 ### Root-Required Tests (Optional for CI)
 
-The cpuset tests (74 tests) can run in CI with special setup:
+The cpuset tests (93 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_show_threads_cpuset.py b/tests/test_show_threads_cpuset.py
new file mode 100755
index 000000000000..fd1c70489cd4
--- /dev/null
+++ b/tests/test_show_threads_cpuset.py
@@ -0,0 +1,245 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# SPDX-License-Identifier: GPL-2.0-only
+"""
+Test suite for tuna show_threads --cpuset filter
+
+Tests the --cpuset filter option added to show_threads command.
+"""
+
+import unittest
+import os
+import sys
+import subprocess
+import time
+from pathlib import Path
+
+# Add tuna to path if running from tests directory
+if Path(__file__).parent.name == 'tests':
+    sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from tuna import cpuset
+
+
+class TestShowThreadsCpusetFilterNonRoot(unittest.TestCase):
+    """Test show_threads --cpuset filter without root (basic tests)"""
+
+    def test_help_shows_cpuset_option(self):
+        """Test that --cpuset option appears in help"""
+        result = subprocess.run(
+            [sys.executable, 'tuna-cmd.py', 'show_threads', '--help'],
+            capture_output=True,
+            text=True
+        )
+        self.assertEqual(result.returncode, 0)
+        self.assertIn('--cpuset', result.stdout)
+        self.assertIn('Show only threads in the specified cpuset', result.stdout)
+
+
[email protected](os.geteuid() == 0, "Requires root permissions")
[email protected](cpuset.CpusetsInit().supported, "Requires cgroup v2 support")
+class TestShowThreadsCpusetFilter(unittest.TestCase):
+    """Test show_threads --cpuset filter with root permissions"""
+
+    @classmethod
+    def setUpClass(cls):
+        """Set up test environment once for all tests"""
+        cls.ci = cpuset.CpusetsInit()
+        cls.test_cpuset_name = 'test_show_filter'
+        cls.sleep_proc = None
+
+    def setUp(self):
+        """Clean up before each test"""
+        # Clean up any leftover test cpusets
+        try:
+            cpuset.destroy_cpuset(self.test_cpuset_name, force=True)
+        except:
+            pass
+
+    def tearDown(self):
+        """Clean up after each test"""
+        # Kill sleep process if running
+        if self.sleep_proc and self.sleep_proc.poll() is None:
+            self.sleep_proc.kill()
+            self.sleep_proc.wait()
+            self.sleep_proc = None
+
+        # Destroy test cpuset
+        try:
+            cpuset.destroy_cpuset(self.test_cpuset_name, force=True)
+        except:
+            pass
+
+    def test_filter_shows_only_cpuset_processes(self):
+        """Test that --cpuset filter shows only processes in specified cpuset"""
+        # Create test cpuset
+        cs = cpuset.Cpuset(self.test_cpuset_name)
+        cs.assign_cpus('0-1')
+        cs.write_memnode('0')
+
+        # Start a sleep process in the background
+        self.sleep_proc = subprocess.Popen(['sleep', '60'])
+        sleep_pid = self.sleep_proc.pid
+        time.sleep(0.1)  # Give process time to start
+
+        # Move sleep process to test cpuset
+        cs.write_pid(sleep_pid)
+        time.sleep(0.1)  # Give cgroup time to update
+
+        # Verify process is in the cpuset
+        with open(f'/proc/{sleep_pid}/cgroup', 'r') as f:
+            cgroup = f.read().strip()
+        self.assertIn(self.test_cpuset_name, cgroup)
+
+        # Run show_threads with cpuset filter
+        result = subprocess.run(
+            [sys.executable, 'tuna-cmd.py', 'show_threads', '--cpuset', self.test_cpuset_name],
+            capture_output=True,
+            text=True
+        )
+
+        self.assertEqual(result.returncode, 0)
+        # Check that sleep process is in output
+        self.assertIn(str(sleep_pid), result.stdout)
+        self.assertIn('sleep', result.stdout)
+
+    def test_filter_with_nonexistent_cpuset(self):
+        """Test that filtering with non-existent cpuset shows no processes"""
+        # Run show_threads with non-existent cpuset
+        result = subprocess.run(
+            [sys.executable, 'tuna-cmd.py', 'show_threads', '--cpuset', 'nonexistent_cpuset'],
+            capture_output=True,
+            text=True
+        )
+
+        self.assertEqual(result.returncode, 0)
+        # Output should be empty or just header
+        lines = [line for line in result.stdout.split('\n') if line.strip()]
+        # Should have at most 2 lines (header rows) and no process entries
+        self.assertLessEqual(len(lines), 2, "Should show no processes for non-existent cpuset")
+
+    def test_filter_excludes_other_cpuset_processes(self):
+        """Test that filter excludes processes from other cpusets"""
+        # Create first test cpuset
+        cs1 = cpuset.Cpuset(self.test_cpuset_name)
+        cs1.assign_cpus('0-1')
+        cs1.write_memnode('0')
+
+        # Create second cpuset
+        cs2_name = f'{self.test_cpuset_name}_2'
+        try:
+            cs2 = cpuset.Cpuset(cs2_name)
+            cs2.assign_cpus('0-1')
+            cs2.write_memnode('0')
+
+            # Start two sleep processes
+            self.sleep_proc = subprocess.Popen(['sleep', '60'])
+            sleep_pid1 = self.sleep_proc.pid
+            sleep_proc2 = subprocess.Popen(['sleep', '60'])
+            sleep_pid2 = sleep_proc2.pid
+            time.sleep(0.1)
+
+            # Move first to test_cpuset_name, second to test_cpuset_name_2
+            cs1.write_pid(sleep_pid1)
+            cs2.write_pid(sleep_pid2)
+            time.sleep(0.1)
+
+            # Filter for first cpuset
+            result = subprocess.run(
+                [sys.executable, 'tuna-cmd.py', 'show_threads', '--cpuset', self.test_cpuset_name],
+                capture_output=True,
+                text=True
+            )
+
+            self.assertEqual(result.returncode, 0)
+            # Should show first process but not second
+            self.assertIn(str(sleep_pid1), result.stdout)
+            self.assertNotIn(str(sleep_pid2), result.stdout)
+
+            # Clean up second process and cpuset
+            sleep_proc2.kill()
+            sleep_proc2.wait()
+
+        finally:
+            # Clean up second cpuset
+            try:
+                cpuset.destroy_cpuset(cs2_name, force=True)
+            except:
+                pass
+
+    def test_filter_with_cgroups_option(self):
+        """Test that --cpuset works together with -G (cgroups display)"""
+        # Create test cpuset
+        cs = cpuset.Cpuset(self.test_cpuset_name)
+        cs.assign_cpus('0-1')
+        cs.write_memnode('0')
+
+        # Start a sleep process
+        self.sleep_proc = subprocess.Popen(['sleep', '60'])
+        sleep_pid = self.sleep_proc.pid
+        time.sleep(0.1)
+
+        # Move to cpuset
+        cs.write_pid(sleep_pid)
+        time.sleep(0.1)
+
+        # Run with both --cpuset and -G
+        result = subprocess.run(
+            [sys.executable, 'tuna-cmd.py', 'show_threads', '--cpuset', self.test_cpuset_name, '-G'],
+            capture_output=True,
+            text=True
+        )
+
+        self.assertEqual(result.returncode, 0)
+        self.assertIn(str(sleep_pid), result.stdout)
+        # Should show cgroup path in output
+        self.assertIn(self.test_cpuset_name, result.stdout)
+
+    def test_filter_with_thread_list(self):
+        """Test that --cpuset can be combined with -t (thread list)"""
+        # Create test cpuset
+        cs = cpuset.Cpuset(self.test_cpuset_name)
+        cs.assign_cpus('0-1')
+        cs.write_memnode('0')
+
+        # Start a sleep process
+        self.sleep_proc = subprocess.Popen(['sleep', '60'])
+        sleep_pid = self.sleep_proc.pid
+        time.sleep(0.1)
+
+        # Move to cpuset
+        cs.write_pid(sleep_pid)
+        time.sleep(0.1)
+
+        # Run with both --cpuset and -t
+        result = subprocess.run(
+            [sys.executable, 'tuna-cmd.py', 'show_threads', '--cpuset', self.test_cpuset_name, '-t', str(sleep_pid)],
+            capture_output=True,
+            text=True
+        )
+
+        self.assertEqual(result.returncode, 0)
+        self.assertIn(str(sleep_pid), result.stdout)
+
+    def test_filter_empty_cpuset(self):
+        """Test filtering on an empty cpuset (no processes)"""
+        # Create empty cpuset
+        cs = cpuset.Cpuset(self.test_cpuset_name)
+        cs.assign_cpus('0-1')
+        cs.write_memnode('0')
+
+        # Filter for empty cpuset
+        result = subprocess.run(
+            [sys.executable, 'tuna-cmd.py', 'show_threads', '--cpuset', self.test_cpuset_name],
+            capture_output=True,
+            text=True
+        )
+
+        self.assertEqual(result.returncode, 0)
+        # Should show no processes
+        lines = [line for line in result.stdout.split('\n') if line.strip()]
+        self.assertLessEqual(len(lines), 2, "Empty cpuset should show no processes")
+
+
+if __name__ == '__main__':
+    unittest.main()
-- 
2.54.0