[PATCH 33/36] tuna: Add tests for cpuset name display in show_threads -G
John Kacur <[email protected]> Fri, 10 Jul 2026 10:15:11 -0400
| Newsgroups | org.kernel.vger.linux-rt-users |
|---|---|
| Message-ID | <[email protected]> |
Add comprehensive test suite for the enhanced cpuset display functionality in 'tuna show_threads -G'. Test coverage: - 7 non-root tests for extract_cpuset_name() function - Basic cpuset extraction: "0::/tuna0" → "tuna0" - Root cgroup: "0::/" → "" - System/user slices: first component extraction - Nested cpusets and edge cases (empty, None) - 4 root-required tests for display behavior - Verify cpuset name display (not full path) - Verify header shows "cpuset" not "cgroup" - Verify root cgroup displays as empty - Verify system.slice processes show shortened names Total new tests: 11 (7 non-root + 4 root-required) Updated test count: 142 tests (45 non-root + 97 root-required) Documentation updates: - Makefile: Added test-show-threads-cgroups target - tests/README.md: Updated test counts and added test descriptions Assisted-by: Claude:claude-sonnet-4-5 Signed-off-by: John Kacur <[email protected]> --- Makefile | 8 +- tests/README.md | 82 +++++++++--- tests/test_show_threads_cgroups.py | 199 +++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 22 deletions(-) create mode 100644 tests/test_show_threads_cgroups.py diff --git a/Makefile b/Makefile index 9e3ffe81bda5..b68c2a2a9925 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-show-threads-cpuset test-all +.PHONY: tests unit-tests test-eperm test-converters test-cpuset test-cpuset-cli test-isolate-cpuset test-show-threads-cpuset test-show-threads-cgroups test-all tests: unit-tests unit-tests: @@ -45,4 +45,8 @@ test-isolate-cpuset: 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 +test-show-threads-cgroups: + @python3 -m unittest tests.test_show_threads_cgroups.TestExtractCpusetName -v + @sudo python3 -m unittest tests.test_show_threads_cgroups.TestShowThreadsCgroupsDisplay -v + +test-all: test-eperm test-converters test-cpuset test-cpuset-cli test-show-threads-cpuset test-show-threads-cgroups diff --git a/tests/README.md b/tests/README.md index f9908326d6f3..799fc1ffe2dd 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 (38 tests) +# Run all non-root tests only (45 tests) make tests -# Run ALL tests (all 131 tests, requires sudo for root-required tests) +# Run ALL tests (all 142 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, 38 tests) +# Using make (non-root tests only, 45 tests) make tests # or make unit-tests -# Using make (ALL tests including root-required tests, 131 tests) +# Using make (ALL tests including root-required tests, 142 tests) make test-all # Using the test runner directly (non-root tests only) @@ -64,6 +64,7 @@ make test-cpuset make test-cpuset-cli make test-isolate-cpuset make test-show-threads-cpuset +make test-show-threads-cgroups # Or using Python unittest directly python3 -m unittest tests.test_eperm_handling -v @@ -72,6 +73,8 @@ 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 +python3 -m unittest tests.test_show_threads_cgroups.TestExtractCpusetName -v +sudo python3 -m unittest tests.test_show_threads_cgroups.TestShowThreadsCgroupsDisplay -v ``` ### Specific Test Class @@ -113,7 +116,7 @@ Tests are organized using Python's `unittest` framework. Each test file contains ### Current Tests -#### Non-Root Tests (38 tests) +#### Non-Root Tests (45 tests) - **test_eperm_handling.py** (6 tests) - Tests for EPERM error handling - Verifies that Permission Denied errors are handled gracefully @@ -155,7 +158,16 @@ Tests are organized using Python's `unittest` framework. Each test file contains - 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_show_threads_cgroups.py::TestExtractCpusetName** (7 tests) - Cpuset name extraction tests + - Tests extract_cpuset_name() function for parsing cgroup paths + - Tests basic cpuset name extraction: "0::/tuna0" → "tuna0" + - Tests root cgroup: "0::/" → "" + - Tests system.slice: "0::/system.slice/ssh.service" → "system.slice" + - Tests user.slice and nested cpusets + - Tests edge cases: empty string and None inputs + - Run: `python3 -m unittest tests.test_show_threads_cgroups.TestExtractCpusetName -v` + +#### Root-Required Tests (97 tests) - **test_process_blocklist.py** (2 tests) - Process blocklist root tests - Tests actual blocking of systemd from being moved to cpusets @@ -199,13 +211,21 @@ Tests are organized using Python's `unittest` framework. Each test file contains - 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_show_threads_cgroups.py** (4 tests) - show_threads -G cpuset display tests + - TestShowThreadsCgroupsDisplay: Tests cpuset name display in show_threads -G output + - test_cgroups_displays_cpuset_name: Verifies -G shows cpuset name, not full path + - test_cgroups_header_shows_cpuset: Verifies header says "cpuset" not "cgroup" + - test_cgroups_displays_root_as_empty: Verifies root cgroup shows empty cpuset + - test_cgroups_displays_system_slice_shortened: Verifies system.slice shows shortened name + - Run: `sudo python3 -m unittest tests.test_show_threads_cgroups.TestShowThreadsCgroupsDisplay -v` + +**Total: 142 tests** (45 non-root + 97 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 (38 tests) +#### Non-Root Tests (45 tests) These tests run without root privileges: @@ -237,13 +257,18 @@ These tests run without root privileges: - Tests that --cpuset option appears in help text - Read-only operation checking command-line interface +7. **Cpuset name extraction tests** verify cgroup path parsing + - Test extract_cpuset_name() function for parsing cgroup paths + - Test various cgroup path formats (cpusets, system.slice, user.slice, etc.) + - Pure logic testing with no system dependencies + **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 (93 tests) +#### Root-Required Tests (97 tests) These tests require root to create/manipulate cgroups: @@ -272,6 +297,13 @@ These tests require root to create/manipulate cgroups: - Test filtering on empty cpusets - All tests create temporary cpusets and processes, then clean up +4. **show_threads -G cpuset display tests** (4 tests) test cpuset name display + - Test that -G displays cpuset name instead of full cgroup path + - Test that header shows "cpuset" instead of "cgroup" + - Test that root cgroup displays as empty string + - Test that system.slice processes show shortened names + - 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 @@ -361,7 +393,7 @@ self.assertRaises(Exception, fn) # fn() raises Exception ## Expected Output -### Non-Root Tests Only (38 tests) +### Non-Root Tests Only (45 tests) Running without sudo will run only the non-root tests: @@ -391,21 +423,28 @@ test_get_numa_nodes_for_empty_list (test_cpuset.TestNumaDetection) ... ok test_get_numa_nodes_for_single_cpu (test_cpuset.TestNumaDetection) ... ok test_numa_node_contains (test_cpuset.TestNumaDetection) ... ok test_numa_node_int (test_cpuset.TestNumaDetection) ... ok +test_extract_cpuset_name_basic (test_show_threads_cgroups.TestExtractCpusetName) ... ok +test_extract_cpuset_name_root (test_show_threads_cgroups.TestExtractCpusetName) ... ok +test_extract_cpuset_name_system_slice (test_show_threads_cgroups.TestExtractCpusetName) ... ok +test_extract_cpuset_name_user_slice (test_show_threads_cgroups.TestExtractCpusetName) ... ok +test_extract_cpuset_name_nested (test_show_threads_cgroups.TestExtractCpusetName) ... ok +test_extract_cpuset_name_empty_string (test_show_threads_cgroups.TestExtractCpusetName) ... ok +test_extract_cpuset_name_none (test_show_threads_cgroups.TestExtractCpusetName) ... ok ... (most cpuset tests skipped: "Requires root permissions") ---------------------------------------------------------------------- -Ran 38 tests in 0.XXXs +Ran 45 tests in 0.XXXs -OK (skipped=93) +OK (skipped=97) ``` -### All Tests Including Root-Required (131 tests) +### All Tests Including Root-Required (142 tests) -Running with sudo will run all 131 tests: +Running with sudo will run all 142 tests: ```bash $ sudo python3 -m unittest discover -s tests -p "test_*.py" -v -... (38 non-root tests as above) +... (45 non-root tests as above) test_initialization (test_cpuset.TestCpusetsInit) ... ok test_cpuset_path (test_cpuset.TestCpusetsInit) ... ok ... (32 cpuset module tests) @@ -413,9 +452,10 @@ test_first_name_is_tuna0 (test_cpuset_cli.TestGetNextTunaCpusetName) ... ok test_sequential_naming (test_cpuset_cli.TestGetNextTunaCpusetName) ... ok ... (45 cpuset CLI tests) ... (6 show_threads --cpuset tests) +... (4 show_threads -G display tests) ---------------------------------------------------------------------- -Ran 131 tests in X.XXXs +Ran 142 tests in X.XXXs OK ``` @@ -424,18 +464,20 @@ OK The test suite includes: -### Unit Tests (38 tests - no root required) +### Unit Tests (45 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) +- **Cpuset name extraction tests**: Cgroup path parsing logic (pure function testing) -### Integration Tests (93 tests - require root) +### Integration Tests (97 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 +- **show_threads -G display tests**: Test cpuset name display in cgroups output Future test categories might include: - **System tests** - Full end-to-end tests requiring CPU affinity operations @@ -446,7 +488,7 @@ Future test categories might include: ### Non-Root Tests (Recommended for CI) -The non-root tests (38 tests) are designed to run in CI/CD environments: +The non-root tests (45 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 @@ -460,7 +502,7 @@ python3 -m unittest discover -s tests -p "test_*.py" -v ### Root-Required Tests (Optional for CI) -The cpuset tests (93 tests) can run in CI with special setup: +The cpuset tests (97 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_cgroups.py b/tests/test_show_threads_cgroups.py new file mode 100644 index 000000000000..67fbb7e5a6ff --- /dev/null +++ b/tests/test_show_threads_cgroups.py @@ -0,0 +1,199 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: GPL-2.0-only +""" +Test suite for tuna show_threads -G cpuset display + +Tests the cpuset name extraction and display in show_threads -G output. +""" + +import unittest +import os +import sys +import subprocess +import time +import importlib.util +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 +import importlib.util + +# Load tuna-cmd.py as a module +spec = importlib.util.spec_from_file_location("tuna_cmd", "tuna-cmd.py") +tuna_cmd = importlib.util.module_from_spec(spec) + + +class TestExtractCpusetName(unittest.TestCase): + """Test extract_cpuset_name() function (non-root tests)""" + + @classmethod + def setUpClass(cls): + """Load the extract_cpuset_name function""" + spec.loader.exec_module(tuna_cmd) + cls.extract_cpuset_name = staticmethod(tuna_cmd.extract_cpuset_name) + + def test_extract_cpuset_name_basic(self): + """Test basic cpuset name extraction""" + result = self.extract_cpuset_name("0::/tuna0") + self.assertEqual(result, "tuna0") + + def test_extract_cpuset_name_root(self): + """Test root cgroup extraction""" + result = self.extract_cpuset_name("0::/") + self.assertEqual(result, "") + + def test_extract_cpuset_name_system_slice(self): + """Test system.slice extraction""" + result = self.extract_cpuset_name("0::/system.slice/ssh.service") + self.assertEqual(result, "system.slice") + + def test_extract_cpuset_name_user_slice(self): + """Test user.slice extraction""" + result = self.extract_cpuset_name("0::/user.slice/user-1000.slice/session-2.scope") + self.assertEqual(result, "user.slice") + + def test_extract_cpuset_name_nested(self): + """Test nested cpuset extraction""" + result = self.extract_cpuset_name("0::/parent/child") + self.assertEqual(result, "parent") + + def test_extract_cpuset_name_empty_string(self): + """Test empty string input""" + result = self.extract_cpuset_name("") + self.assertEqual(result, "") + + def test_extract_cpuset_name_none(self): + """Test None input""" + result = self.extract_cpuset_name(None) + self.assertEqual(result, "") + + [email protected](os.geteuid() == 0, "Requires root permissions") [email protected](cpuset.CpusetsInit().supported, "Requires cgroup v2 support") +class TestShowThreadsCgroupsDisplay(unittest.TestCase): + """Test show_threads -G cpuset display with root permissions""" + + @classmethod + def setUpClass(cls): + """Set up test environment once for all tests""" + cls.ci = cpuset.CpusetsInit() + cls.test_cpuset_name = 'test_cgroups_display' + 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_cgroups_displays_cpuset_name(self): + """Test that -G displays cpuset name (not full path)""" + # 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 show_threads with -G + result = subprocess.run( + [sys.executable, 'tuna-cmd.py', 'show_threads', '-G', '-t', str(sleep_pid)], + capture_output=True, + text=True + ) + + self.assertEqual(result.returncode, 0) + # Should show cpuset name, not full path + self.assertIn(self.test_cpuset_name, result.stdout) + # Should NOT show "0::/" prefix + self.assertNotIn("0::", result.stdout) + + def test_cgroups_header_shows_cpuset(self): + """Test that -G header shows 'cpuset' not 'cgroup'""" + # Import necessary modules to test the header function directly + import io + import sys as test_sys + from importlib import reload + + # Reload tuna_cmd to get fresh import + spec = importlib.util.spec_from_file_location("tuna_cmd", "tuna-cmd.py") + tuna_cmd = importlib.util.module_from_spec(spec) + spec.loader.exec_module(tuna_cmd) + + # Initialize i18n to set up the _() function + tuna_cmd.i18n_init() + + # Capture output from ps_show_header + old_stdout = test_sys.stdout + test_sys.stdout = io.StringIO() + + try: + # Call ps_show_header with cgroups=True + tuna_cmd.ps_show_header(has_ctxt_switch_info=False, cgroups=True) + output = test_sys.stdout.getvalue() + + # Header should say "cpuset" not "cgroup" + self.assertIn("cpuset", output) + finally: + test_sys.stdout = old_stdout + + def test_cgroups_displays_root_as_empty(self): + """Test that processes in root cgroup show empty cpuset""" + # Get a process that's likely in root cgroup (PID 1 - systemd) + result = subprocess.run( + [sys.executable, 'tuna-cmd.py', 'show_threads', '-G', '-t', '1'], + capture_output=True, + text=True + ) + + self.assertEqual(result.returncode, 0) + # Should NOT show "0::/" for root cgroup + self.assertNotIn("0::/", result.stdout) + + def test_cgroups_displays_system_slice_shortened(self): + """Test that system.slice processes show shortened name""" + # Find a process in system.slice (many system daemons are there) + # We'll look for any process and check the display format + result = subprocess.run( + [sys.executable, 'tuna-cmd.py', 'show_threads', '-G'], + capture_output=True, + text=True + ) + + self.assertEqual(result.returncode, 0) + # If any system.slice processes exist, they should show "system.slice" + # not the full path like "0::/system.slice/service.service" + if "system.slice" in result.stdout: + # Should NOT show "0::/" prefix + self.assertNotIn("0::/system.slice/", result.stdout) + + +if __name__ == '__main__': + unittest.main() -- 2.54.0