[PATCH 19/36] tuna: Add tests for cpuset show and status commands
John Kacur <[email protected]> Fri, 10 Jul 2026 10:14:57 -0400
| Newsgroups | org.kernel.vger.linux-rt-users |
|---|---|
| Message-ID | <[email protected]> |
Add comprehensive test coverage for the new cpuset show and status commands: - TestCpusetShowCLI (4 tests): * test_show_basic_info - Verify basic cpuset information display * test_show_nonexistent_cpuset - Error handling for missing cpusets * test_show_with_tasks - Task list display functionality * test_get_cpuset_info_error_handling - Error handling in helper - TestCpusetStatusCLI (4 tests): * test_status_with_multiple_cpusets - Multiple cpusets display * test_status_with_pattern_filter - Pattern filtering functionality * test_status_skip_empty - Skip empty cpusets option * test_status_shows_isolated - Isolated cpuset identification Update tests/README.md to reflect new test counts: - Total tests: 91 (up from 71) - Non-root tests: 37 (unchanged) - Root-required tests: 54 (up from 42) All tests follow the existing patterns with proper setup/teardown and cleanup to ensure no test cpusets are left behind. Assisted-by: Claude:claude-sonnet-4-5 Signed-off-by: John Kacur <[email protected]> --- tests/README.md | 53 ++++++------ tests/test_cpuset_cli.py | 175 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 25 deletions(-) diff --git a/tests/README.md b/tests/README.md index a199f7efa9e6..c890b0ab0c78 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 (29 tests) +# Run all non-root tests only (37 tests) make tests -# Run ALL tests (all 71 tests, requires sudo for root-required tests) +# Run ALL tests (all 91 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, 29 tests) +# Using make (non-root tests only, 37 tests) make tests # or make unit-tests -# Using make (ALL tests including root-required tests, 71 tests) +# Using make (ALL tests including root-required tests, 91 tests) make test-all # Using the test runner directly (non-root tests only) @@ -144,32 +144,35 @@ 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 (44 tests) +#### Root-Required Tests (54 tests) - **test_process_blocklist.py** (2 tests) - Process blocklist root tests - Tests actual blocking of systemd from being moved to cpusets - Verifies normal processes can still be moved - Run: `sudo python3 -m unittest tests.test_process_blocklist.TestProcessBlocklist.test_write_pid_blocks_systemd tests.test_process_blocklist.TestProcessBlocklist.test_write_pid_allows_normal_process -v` -- **test_cpuset.py** (22 tests) - Core cpuset module tests +- **test_cpuset.py** (25 tests) - Core cpuset module tests - TestCpusetCreation: Creating and destroying cpusets - TestCpusetConfiguration: CPU/memory assignment - TestContextManager: auto_destroy behavior with context managers + - TestProcessMovement: write_pid() functionality - TestTaskMigration: Process migration between cpusets - TestDiscoveryFunctions: list_cpusets() with patterns and recursion - TestCleanupFunctions: destroy_cpuset() and cleanup_cpusets() - - 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) + - 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 36 tests with root) -- **test_cpuset_cli.py** (22 tests) - CLI command tests +- **test_cpuset_cli.py** (30 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 - TestCpusetDestroyCLI: Destroy command with patterns and safety options - TestCpusetMoveCLI: Move command with --pids, --threads, error handling, blocklist protection + - TestCpusetShowCLI: Show command for detailed cpuset information + - TestCpusetStatusCLI: Status command for system-wide cpuset overview - Run: `sudo python3 -m unittest tests.test_cpuset_cli -v` -**Total: 83 tests** (37 non-root + 46 root-required) +**Total: 91 tests** (37 non-root + 54 root-required) ### Test Requirements: Root vs Non-Root @@ -209,11 +212,11 @@ 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 (42 tests) +#### Root-Required Tests (54 tests) These tests require root to create/manipulate cgroups: -1. **Cpuset module tests** (22 tests) create actual cgroups in /sys/fs/cgroup +1. **Cpuset module tests** (25 tests) create actual cgroups in /sys/fs/cgroup - Test real cgroup v2 operations - Verify CPU assignment, memory node configuration - Test task migration between cpusets @@ -316,7 +319,7 @@ self.assertRaises(Exception, fn) # fn() raises Exception ## Expected Output -### Non-Root Tests Only (29 tests) +### Non-Root Tests Only (37 tests) Running without sudo will run only the non-root tests: @@ -349,27 +352,27 @@ test_numa_node_int (test_cpuset.TestNumaDetection) ... ok ... (most cpuset tests skipped: "Requires root permissions") ---------------------------------------------------------------------- -Ran 29 tests in 0.XXXs +Ran 37 tests in 0.XXXs -OK (skipped=42) +OK (skipped=54) ``` -### All Tests Including Root-Required (71 tests) +### All Tests Including Root-Required (91 tests) -Running with sudo will run all 71 tests: +Running with sudo will run all 91 tests: ```bash $ sudo python3 -m unittest discover -s tests -p "test_*.py" -v -... (29 non-root tests as above) +... (37 non-root tests as above) test_initialization (test_cpuset.TestCpusetsInit) ... ok test_cpuset_path (test_cpuset.TestCpusetsInit) ... ok -... (27 cpuset module tests) +... (25 cpuset module tests) test_first_name_is_tuna0 (test_cpuset_cli.TestGetNextTunaCpusetName) ... ok test_sequential_naming (test_cpuset_cli.TestGetNextTunaCpusetName) ... ok -... (20 cpuset CLI tests) +... (28 cpuset CLI tests) ---------------------------------------------------------------------- -Ran 71 tests in X.XXXs +Ran 91 tests in X.XXXs OK ``` @@ -378,13 +381,13 @@ OK The test suite includes: -### Unit Tests (29 tests - no root required) +### Unit Tests (37 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) -### Integration Tests (42 tests - require root) +### Integration Tests (54 tests - require root) - **Cpuset module tests**: Test interaction with cgroup v2 subsystem - **Cpuset CLI tests**: Test command-line interface integration @@ -397,7 +400,7 @@ Future test categories might include: ### Non-Root Tests (Recommended for CI) -The non-root tests (29 tests) are designed to run in CI/CD environments: +The non-root tests (37 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 @@ -411,7 +414,7 @@ python3 -m unittest discover -s tests -p "test_*.py" -v ### Root-Required Tests (Optional for CI) -The cpuset tests (42 tests) can run in CI with special setup: +The cpuset tests (54 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_cli.py b/tests/test_cpuset_cli.py index 77b7fcbe1a4f..51c9b5d193c0 100644 --- a/tests/test_cpuset_cli.py +++ b/tests/test_cpuset_cli.py @@ -8,6 +8,9 @@ Tests the command-line interface functions added to tuna-cmd.py: - cpuset create (with auto-naming, custom names, --isolated) - cpuset list (with --pattern, --verbose, --skip-empty) - cpuset destroy (single and pattern-based, with --skip-empty) +- cpuset move (moving processes to cpusets) +- cpuset show (detailed information about a specific cpuset) +- cpuset status (system-wide overview of all cpusets) - get_next_tuna_cpuset_name() auto-naming logic All tests clean up created cpusets afterwards (even though tuna creates persistent cpusets). @@ -38,6 +41,10 @@ cpuset_create = tuna_cmd.cpuset_create cpuset_list = tuna_cmd.cpuset_list cpuset_destroy = tuna_cmd.cpuset_destroy cpuset_move = tuna_cmd.cpuset_move +cpuset_show = tuna_cmd.cpuset_show +cpuset_status = tuna_cmd.cpuset_status +get_cpuset_info = tuna_cmd.get_cpuset_info +get_all_cpusets_info = tuna_cmd.get_all_cpusets_info @unittest.skipUnless(os.geteuid() == 0, "Requires root permissions") @@ -477,6 +484,172 @@ class TestCpusetMoveCLI(unittest.TestCase): self.assertIn(str(current_pid), pids, "Normal process should be moved") [email protected](os.geteuid() == 0, "Requires root permissions") [email protected](cpuset.CpusetsInit().supported, "Requires cgroup v2 support") +class TestCpusetShowCLI(unittest.TestCase): + """Test 'tuna cpuset show' command""" + + def setUp(self): + """Create test cpusets before each test""" + self.ci = cpuset.CpusetsInit() + # Clean up any existing test cpusets + try: + cpuset.cleanup_cpusets('test_show_*', force=True, recursive=True) + except: + pass + + def tearDown(self): + """Clean up test cpusets after each test""" + try: + cpuset.cleanup_cpusets('test_show_*', force=True, recursive=True) + except: + pass + + def test_show_basic_info(self): + """Test showing basic cpuset information""" + # Create a test cpuset + cs = cpuset.Cpuset('test_show_basic') + cs.write_memnode('0') + cs.assign_cpus('0-1') + + # Get info using the helper function + info = get_cpuset_info('test_show_basic') + + self.assertEqual(info['name'], 'test_show_basic') + self.assertEqual(info['cpus'], '0-1') + self.assertEqual(info['mems'], '0') + self.assertIsNone(info['error']) + + def test_show_nonexistent_cpuset(self): + """Test showing info for nonexistent cpuset""" + # Try to show a cpuset that doesn't exist + with self.assertRaises(SystemExit): + # Capture stdout to avoid cluttering test output + import io + import contextlib + f = io.StringIO() + with contextlib.redirect_stderr(f): + cpuset_show('test_show_nonexistent', show_tasks=False) + + def test_show_with_tasks(self): + """Test showing cpuset with tasks""" + # Create a test cpuset and add current process + cs = cpuset.Cpuset('test_show_tasks') + cs.write_memnode('0') + cs.assign_cpus('0-1') + cs.write_pid(os.getpid()) + + # Get info + info = get_cpuset_info('test_show_tasks') + + self.assertGreater(info['task_count'], 0) + self.assertIn(str(os.getpid()), info['tasks']) + + def test_get_cpuset_info_error_handling(self): + """Test get_cpuset_info with invalid cpuset""" + info = get_cpuset_info('nonexistent_cpuset') + + self.assertIsNotNone(info['error']) + self.assertEqual(info['name'], 'nonexistent_cpuset') + + [email protected](os.geteuid() == 0, "Requires root permissions") [email protected](cpuset.CpusetsInit().supported, "Requires cgroup v2 support") +class TestCpusetStatusCLI(unittest.TestCase): + """Test 'tuna cpuset status' command""" + + def setUp(self): + """Create test cpusets before each test""" + self.ci = cpuset.CpusetsInit() + # Clean up any existing test cpusets + try: + cpuset.cleanup_cpusets('test_status_*', force=True, recursive=True) + except: + pass + + def tearDown(self): + """Clean up test cpusets after each test""" + try: + cpuset.cleanup_cpusets('test_status_*', force=True, recursive=True) + except: + pass + + def test_status_with_multiple_cpusets(self): + """Test status showing multiple cpusets""" + # Create multiple test cpusets + cs1 = cpuset.Cpuset('test_status_1') + cs1.write_memnode('0') + cs1.assign_cpus('0-1') + + cs2 = cpuset.Cpuset('test_status_2') + cs2.write_memnode('0') + cs2.assign_cpus('2-3') + cs2.set_cpu_exclusive() + + # Get all cpusets info + infos = get_all_cpusets_info(pattern='test_status_*', recursive=False) + + self.assertEqual(len(infos), 2) + names = [info['name'] for info in infos] + self.assertIn('test_status_1', names) + self.assertIn('test_status_2', names) + + def test_status_with_pattern_filter(self): + """Test status with pattern filtering""" + # Create cpusets with different patterns + cs1 = cpuset.Cpuset('test_status_match') + cs1.write_memnode('0') + cs1.assign_cpus('0') + + cs2 = cpuset.Cpuset('test_other_nomatch') + cs2.write_memnode('0') + cs2.assign_cpus('1') + + # Get cpusets matching pattern + infos = get_all_cpusets_info(pattern='test_status_*', recursive=False) + + self.assertEqual(len(infos), 1) + self.assertEqual(infos[0]['name'], 'test_status_match') + + # Clean up the non-matching one manually + try: + cpuset.destroy_cpuset('test_other_nomatch', force=True) + except: + pass + + def test_status_skip_empty(self): + """Test status with skip_empty option""" + # Create cpuset with CPUs + cs1 = cpuset.Cpuset('test_status_filled') + cs1.write_memnode('0') + cs1.assign_cpus('0-1') + + # Create cpuset without CPUs + cs2 = cpuset.Cpuset('test_status_empty') + cs2.write_memnode('0') + + # Get all (should include both) + all_infos = get_all_cpusets_info(pattern='test_status_*', skip_empty=False) + self.assertEqual(len(all_infos), 2) + + # Get non-empty only + nonempty_infos = get_all_cpusets_info(pattern='test_status_*', skip_empty=True) + self.assertEqual(len(nonempty_infos), 1) + self.assertEqual(nonempty_infos[0]['name'], 'test_status_filled') + + def test_status_shows_isolated(self): + """Test status correctly identifies isolated cpusets""" + # Create isolated cpuset + cs = cpuset.Cpuset('test_status_isolated') + cs.write_memnode('0') + cs.assign_cpus('0-1') + cs.set_cpu_exclusive() + + info = get_cpuset_info('test_status_isolated') + + self.assertEqual(info['partition'], 'isolated') + + def suite(): """Create test suite""" loader = unittest.TestLoader() @@ -488,6 +661,8 @@ def suite(): suite.addTests(loader.loadTestsFromTestCase(TestCpusetListCLI)) suite.addTests(loader.loadTestsFromTestCase(TestCpusetDestroyCLI)) suite.addTests(loader.loadTestsFromTestCase(TestCpusetMoveCLI)) + suite.addTests(loader.loadTestsFromTestCase(TestCpusetShowCLI)) + suite.addTests(loader.loadTestsFromTestCase(TestCpusetStatusCLI)) return suite -- 2.54.0