[PATCH 15/36] tuna: Add tests for NUMA-aware memory node assignment
John Kacur <[email protected]> Fri, 10 Jul 2026 10:14:53 -0400
| Newsgroups | org.kernel.vger.linux-rt-users |
|---|---|
| Message-ID | <[email protected]> |
Add comprehensive test coverage for the NUMA detection and auto-assignment functionality added to cpusets. Changes to tests/test_cpuset.py: - Add TestNumaDetection class with 6 tests - test_discover_numa_nodes: Validates NUMA topology discovery - test_get_numa_nodes_for_cpus_basic: Tests memory node detection - test_get_numa_nodes_for_empty_list: Tests empty CPU list fallback - test_get_numa_nodes_for_single_cpu: Tests single CPU detection - test_numa_node_contains: Tests NumaNode __contains__ operator - test_numa_node_int: Tests NumaNode __int__ operator - All tests run without root (read-only sysfs access) - Tests work on both single-node and multi-node systems Changes to tests/test_cpuset_cli.py: - Add test_create_with_numa_autodetect: Tests auto-detection path - Add test_create_with_numa_manual: Tests --memory-nodes override - Both tests validate cpuset.mems is set correctly Changes to tests/README.md: - Update test counts: 63 → 71 total (29 non-root + 42 root-required) - Document TestNumaDetection class and its 6 tests - Update TestCpusetCreateCLI description to mention NUMA tests - Update all example outputs to reflect new test counts All 8 new tests pass successfully. Assisted-by: Claude:claude-sonnet-4-5 Signed-off-by: John Kacur <[email protected]> --- tests/README.md | 71 ++++++++++++++++++++++++++-------------- tests/test_cpuset.py | 70 +++++++++++++++++++++++++++++++++++++++ tests/test_cpuset_cli.py | 27 +++++++++++++++ 3 files changed, 143 insertions(+), 25 deletions(-) diff --git a/tests/README.md b/tests/README.md index ea54d2cee5fd..148d50447420 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 (23 tests) +# Run all non-root tests only (29 tests) make tests -# Run ALL tests (all 63 tests, requires sudo for root-required tests) +# Run ALL tests (all 71 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, 23 tests) +# Using make (non-root tests only, 29 tests) make tests # or make unit-tests -# Using make (ALL tests including root-required tests, 63 tests) +# Using make (ALL tests including root-required tests, 71 tests) make test-all # Using the test runner directly (non-root tests only) @@ -107,7 +107,7 @@ Tests are organized using Python's `unittest` framework. Each test file contains ### Current Tests -#### Non-Root Tests (23 tests) +#### Non-Root Tests (29 tests) - **test_eperm_handling.py** (6 tests) - Tests for EPERM error handling - Verifies that Permission Denied errors are handled gracefully @@ -129,7 +129,15 @@ Tests are organized using Python's `unittest` framework. Each test file contains - These tests can run without root as they only check system capabilities - Run: `python3 -m unittest tests.test_cpuset.TestCpusetsInit -v` -#### Root-Required Tests (40 tests) +- **test_cpuset.py::TestNumaDetection** (6 tests) - NUMA topology detection + - Tests `discover_numa_nodes()`: NUMA node discovery from sysfs + - Tests `get_numa_nodes_for_cpus()`: Auto-detecting memory nodes for CPU lists + - Tests NumaNode class operators (__contains__, __int__) + - Tests single-node and multi-node system support + - Validates smart NUMA-aware memory node assignment for cpusets + - Run: `python3 -m unittest tests.test_cpuset.TestNumaDetection -v` + +#### Root-Required Tests (42 tests) - **test_cpuset.py** (22 tests) - Core cpuset module tests - TestCpusetCreation: Creating and destroying cpusets @@ -141,21 +149,21 @@ Tests are organized using Python's `unittest` framework. Each test file contains - 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) -- **test_cpuset_cli.py** (18 tests) - CLI command tests +- **test_cpuset_cli.py** (20 tests) - CLI command tests - TestGetNextTunaCpusetName: Auto-naming logic (tuna0, tuna1, gap-filling) - - TestCpusetCreateCLI: Create command with auto/custom names and --isolated + - 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 - Run: `sudo python3 -m unittest tests.test_cpuset_cli -v` -**Total: 63 tests** (23 non-root + 40 root-required) +**Total: 71 tests** (29 non-root + 42 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 (23 tests) +#### Non-Root Tests (29 tests) These tests run without root privileges: @@ -173,13 +181,18 @@ These tests run without root privileges: - Check NUMA node configuration - Read-only operations that don't require root +4. **NUMA detection tests** verify NUMA topology discovery + - Test NUMA node discovery from sysfs + - Test smart memory node assignment for CPU lists + - Read-only operations that work on any system (single or multi-node) + **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 (40 tests) +#### Root-Required Tests (42 tests) These tests require root to create/manipulate cgroups: @@ -189,9 +202,10 @@ 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** (18 tests) test command-line interface functions +2. **Cpuset CLI tests** (20 tests) test command-line interface functions - Test cpuset create/list/destroy/move 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 - All tests clean up created cpusets afterwards @@ -285,7 +299,7 @@ self.assertRaises(Exception, fn) # fn() raises Exception ## Expected Output -### Non-Root Tests Only (23 tests) +### Non-Root Tests Only (29 tests) Running without sudo will run only the non-root tests: @@ -309,30 +323,36 @@ test_mixed_numeric_and_pattern (test_converters.TestThreadstringToList) ... ok test_multiple_numeric_pids (test_converters.TestThreadstringToList) ... ok test_pattern_with_mock_ps (test_converters.TestThreadstringToList) ... ok test_single_numeric_pid (test_converters.TestThreadstringToList) ... ok +test_discover_numa_nodes (test_cpuset.TestNumaDetection) ... ok +test_get_numa_nodes_for_cpus_basic (test_cpuset.TestNumaDetection) ... ok +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 ... (most cpuset tests skipped: "Requires root permissions") ---------------------------------------------------------------------- -Ran 23 tests in 0.XXXs +Ran 29 tests in 0.XXXs -OK (skipped=40) +OK (skipped=42) ``` -### All Tests Including Root-Required (63 tests) +### All Tests Including Root-Required (71 tests) -Running with sudo will run all 63 tests: +Running with sudo will run all 71 tests: ```bash $ sudo python3 -m unittest discover -s tests -p "test_*.py" -v -... (23 non-root tests as above) +... (29 non-root tests as above) test_initialization (test_cpuset.TestCpusetsInit) ... ok test_cpuset_path (test_cpuset.TestCpusetsInit) ... ok ... (27 cpuset module tests) test_first_name_is_tuna0 (test_cpuset_cli.TestGetNextTunaCpusetName) ... ok test_sequential_naming (test_cpuset_cli.TestGetNextTunaCpusetName) ... ok -... (18 cpuset CLI tests) +... (20 cpuset CLI tests) ---------------------------------------------------------------------- -Ran 63 tests in X.XXXs +Ran 71 tests in X.XXXs OK ``` @@ -341,12 +361,13 @@ OK The test suite includes: -### Unit Tests (23 tests - no root required) +### Unit Tests (29 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 (40 tests - require root) +### Integration Tests (42 tests - require root) - **Cpuset module tests**: Test interaction with cgroup v2 subsystem - **Cpuset CLI tests**: Test command-line interface integration @@ -359,12 +380,12 @@ Future test categories might include: ### Non-Root Tests (Recommended for CI) -The non-root tests (23 tests) are designed to run in CI/CD environments: +The non-root tests (29 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 - Fast execution (< 1 second) -- Includes basic cpuset capability detection tests +- Includes basic cpuset capability detection and NUMA detection tests ```bash # CI-friendly test command @@ -373,7 +394,7 @@ python3 -m unittest discover -s tests -p "test_*.py" -v ### Root-Required Tests (Optional for CI) -The cpuset tests (40 tests) can run in CI with special setup: +The cpuset tests (42 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.py b/tests/test_cpuset.py index ec6ce19028cf..ca0a6b0c9ccd 100644 --- a/tests/test_cpuset.py +++ b/tests/test_cpuset.py @@ -654,6 +654,75 @@ class TestProcessMovement(unittest.TestCase): cpuset.destroy_cpuset('test_multi_pid', force=True) +class TestNumaDetection(unittest.TestCase): + """Test NUMA topology detection functions""" + + def test_discover_numa_nodes(self): + """Test that NUMA node discovery returns valid data""" + nodes = cpuset.discover_numa_nodes() + + # Should have at least one node + self.assertGreaterEqual(len(nodes), 1) + + # All nodes should have node IDs starting from 0 + self.assertIn(0, nodes) + + # Each node should have CPUs + for nodeid, node in nodes.items(): + self.assertIsInstance(nodeid, int) + self.assertIsInstance(node, cpuset.NumaNode) + self.assertEqual(node.nodeid, nodeid) + self.assertIsInstance(node.cpus, set) + + def test_get_numa_nodes_for_cpus_basic(self): + """Test NUMA node detection for basic CPU lists""" + # Get available nodes + nodes = cpuset.discover_numa_nodes() + + # Get some CPUs from first node + first_node = nodes[min(nodes.keys())] + test_cpus = list(first_node.cpus)[:4] if len(first_node.cpus) >= 4 else list(first_node.cpus) + + if test_cpus: + result = cpuset.get_numa_nodes_for_cpus(test_cpus) + # Should return a string + self.assertIsInstance(result, str) + # Should contain at least the first node + self.assertIn(str(min(nodes.keys())), result) + + def test_get_numa_nodes_for_empty_list(self): + """Test NUMA node detection for empty CPU list""" + result = cpuset.get_numa_nodes_for_cpus([]) + # Should default to "0" + self.assertEqual(result, "0") + + def test_get_numa_nodes_for_single_cpu(self): + """Test NUMA node detection for single CPU""" + # Get first available CPU + nodes = cpuset.discover_numa_nodes() + first_node = nodes[min(nodes.keys())] + + if first_node.cpus: + test_cpu = list(first_node.cpus)[0] + result = cpuset.get_numa_nodes_for_cpus([test_cpu]) + # Should return a valid node string + self.assertIsInstance(result, str) + self.assertTrue(len(result) > 0) + + def test_numa_node_contains(self): + """Test NumaNode __contains__ operator""" + node = cpuset.NumaNode(0, {0, 1, 2, 3}) + self.assertIn(0, node) + self.assertIn(3, node) + self.assertNotIn(4, node) + self.assertNotIn(100, node) + + def test_numa_node_int(self): + """Test NumaNode __int__ operator""" + node = cpuset.NumaNode(5, {0, 1}) + self.assertEqual(int(node), 5) + + def suite(): """Create test suite""" loader = unittest.TestLoader() @@ -661,6 +730,7 @@ def suite(): # Add all test classes suite.addTests(loader.loadTestsFromTestCase(TestCpusetsInit)) + suite.addTests(loader.loadTestsFromTestCase(TestNumaDetection)) suite.addTests(loader.loadTestsFromTestCase(TestCpusetCreation)) suite.addTests(loader.loadTestsFromTestCase(TestCpusetConfiguration)) suite.addTests(loader.loadTestsFromTestCase(TestContextManager)) diff --git a/tests/test_cpuset_cli.py b/tests/test_cpuset_cli.py index 9bec8955d1ad..ac2197c77379 100644 --- a/tests/test_cpuset_cli.py +++ b/tests/test_cpuset_cli.py @@ -160,6 +160,33 @@ class TestCpusetCreateCLI(unittest.TestCase): # Should be 'isolated' (or possibly 'root'/'member' if not supported) self.assertIn(partition, ['isolated', 'root', 'member']) + def test_create_with_numa_autodetect(self): + """Test creating cpuset with NUMA auto-detection (no --memory-nodes)""" + cpuset_create(cpu_list=[0, 1], name='test_numa_auto', isolated=False, memory_nodes=None) + self.created_cpusets.append('test_numa_auto') + + # Verify cpuset exists + self.assertTrue(os.path.exists('/sys/fs/cgroup/test_numa_auto')) + + # Verify memory nodes are set (should be auto-detected) + with open('/sys/fs/cgroup/test_numa_auto/cpuset.mems') as f: + mems = f.read().strip() + # Should have at least one memory node set + self.assertTrue(len(mems) > 0) + # On single-node system, should be "0" + # On multi-node, could be "0", "1", "0-1", etc. + self.assertRegex(mems, r'^[0-9,-]+$') + + def test_create_with_numa_manual(self): + """Test creating cpuset with manual --memory-nodes override""" + cpuset_create(cpu_list=[0, 1], name='test_numa_manual', isolated=False, memory_nodes='0') + self.created_cpusets.append('test_numa_manual') + + # Verify memory nodes are set to what we specified + with open('/sys/fs/cgroup/test_numa_manual/cpuset.mems') as f: + mems = f.read().strip() + self.assertEqual(mems, '0') + @unittest.skipUnless(os.geteuid() == 0, "Requires root permissions") @unittest.skipUnless(cpuset.CpusetsInit().supported, "Requires cgroup v2 support") -- 2.54.0