# SPDX-License-Identifier: GPL-2.0-only

# Copyright (C) 2026 John Kacur

# Tuna Test Suite

This directory contains the test suite for tuna, using Python's `unittest` framework.

## Quick Start

```bash
# Run all non-root tests only (45 tests)
make tests

# Run ALL tests (all 159 tests, requires sudo for root-required tests)
make test-all

# Run specific test module
make test-converters       # Non-root
make test-cpuset          # Root-required
make test-cpuset-cli      # Root-required

# Or use Python unittest directly
python3 -m unittest tests.test_converters -v
sudo python3 -m unittest tests.test_cpuset -v
```

## Running Tests

### All Tests

Run all unit tests using any of these methods:

```bash
# Using make (non-root tests only, 45 tests)
make tests
# or
make unit-tests

# Using make (ALL tests including root-required tests, 159 tests)
make test-all

# Using the test runner directly (non-root tests only)
./tests/run_tests.sh

# Using Python unittest directly (non-root tests only)
python3 -m unittest discover -s tests -p "test_*.py" -v

# Using Python unittest directly (ALL tests including root-required)
sudo python3 -m unittest discover -s tests -p "test_*.py" -v
```

### Specific Test File

Run a specific test file:

```bash
# Using make (non-root tests)
make test-eperm
make test-converters

# Using make (root-required tests)
make test-cpuset
make test-cpuset-cli
make test-isolate-cpuset
make test-run-cpuset
make test-show-threads-cpuset
make test-show-threads-cgroups

# 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
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

Run a specific test class:

```bash
# Non-root tests
python3 -m unittest tests.test_converters.TestThreadstringToList -v
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

Run a single test method:

```bash
# Non-root tests
python3 -m unittest tests.test_eperm_handling.TestEPERMHandling.test_set_affinity_eperm -v
python3 -m unittest tests.test_converters.TestThreadstringToList.test_pattern_with_mock_ps -v

# 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

Tests are organized using Python's `unittest` framework. Each test file contains:
- A test class that inherits from `unittest.TestCase`
- Individual test methods (prefixed with `test_`)
- Setup/teardown methods if needed

### Current 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
  - Tests reading and setting affinity, scheduler, and priority on PID 1
  - Ensures tuna handles permission errors without crashing
  - Run: `python3 -m unittest tests.test_eperm_handling -v`

- **test_process_blocklist.py** (8 tests) - Process blocklist functionality
  - Verifies that critical system processes (systemd, systemd-logind, etc.) are protected
  - Tests that blocklisted processes cannot be moved to custom cpusets
  - Prevents system instability and shutdown issues caused by moving critical processes
  - 6 non-root tests verify blocklist logic, 2 root tests verify actual blocking
  - Run: `python3 -m unittest tests.test_process_blocklist -v`

- **test_converters.py** (12 tests) - Tests for argument converter functions
  - Tests `threadstring_to_list()`: Converting thread specifications to PID lists
  - Tests `irqstring_to_list()`: Converting IRQ specifications to IRQ number lists
  - Tests `socketstring_to_list()`: Socket string conversion
  - Uses MockPidStats to test without requiring real process access
  - Run: `python3 -m unittest tests.test_converters -v`

- **test_cpuset.py::TestCpusetsInit** (5 tests) - CpusetsInit and cgroup v2 detection
  - Tests initialization and cgroup v2 support detection
  - Verifies cpuset path configuration
  - Tests NUMA node detection
  - These tests can run without root as they only check system capabilities
  - Run: `python3 -m unittest tests.test_cpuset.TestCpusetsInit -v`

- **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`

- **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`

- **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 (114 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** (32 tests) - Core cpuset module tests
  - TestCpusetCreation: Creating and destroying cpusets
  - TestCpusetConfiguration: CPU/memory assignment, including modify_cpus() with add/remove operations
  - 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) and TestNumaDetection (6 tests) run without root, see non-root section
  - Run: `sudo python3 -m unittest tests.test_cpuset -v` (runs all 43 tests with root)

- **test_cpuset_cli.py** (51 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
  - 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 (9 tests)
    - Tests cpuset-based CPU isolation with isolated and housekeeping cpusets
    - Verifies partition types: isolated cpuset uses partition=isolated, housekeeping uses partition=member by default
    - Tests --housekeeping-isolated flag to override default and make housekeeping partition=isolated
    - Verifies CPU assignments and process migration
    - Tests error handling and backward compatibility
  - TestTunaRunCpusetCLI: Integration of --cpuset with tuna run command (5 tests)
    - test_run_with_cpuset_basic: Verifies running commands in cpusets
    - test_run_with_cpuset_and_priority: Tests combining cpuset with priority settings
    - test_run_with_nonexistent_cpuset: Tests error handling for non-existent cpusets
    - test_run_cpuset_mutually_exclusive_with_cpus: Tests mutual exclusivity with -c
    - test_run_help_shows_cpuset_option: Verifies --cpuset appears in help
  - Run: `sudo python3 -m unittest tests.test_cpuset_cli -v`

- **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`

- **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`

- **test_save_apply.py** (12 tests) - save/apply cpuset configuration tests
  - TestSaveApply: Tests cpuset save and apply functionality
    - test_save_creates_yaml_file: Verify save creates output file
    - test_save_captures_all_cpusets: Verify all user cpusets are saved
    - test_save_captures_partition_types: Verify isolated vs member preserved
    - test_save_captures_cpu_ranges: Verify CPU assignments preserved
    - test_apply_recreates_cpusets: Verify apply recreates from YAML
    - test_apply_preserves_partition_types: Verify partition types restored
    - test_apply_preserves_cpu_assignments: Verify CPU assignments restored
    - test_apply_with_existing_cpusets_warns: Verify idempotent operation
    - test_apply_missing_file_returns_error: Error handling for missing files
    - test_roundtrip_preserves_configuration: Full save→destroy→apply→verify
    - test_save_with_no_cpusets: Handle empty cpuset case gracefully
    - test_save_skips_system_cpusets: Verify system cpuset filtering
  - Run: `sudo python3 -m unittest tests.test_save_apply -v`

**Total: 159 tests** (45 non-root + 114 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 (45 tests)

These tests run without root privileges:

1. **EPERM tests** verify that permission errors are handled gracefully when running as non-root
   - They intentionally trigger permission errors by trying to modify PID 1
   - Success means the error was caught and handled properly

2. **Process blocklist tests** (6 non-root tests) verify the blocklist logic
   - Test that critical processes (systemd, systemd-logind, etc.) are identified correctly
   - Verify the blocklist prevents system instability and shutdown issues
   - No actual process movement required - tests the detection logic

3. **Converter tests** use mock objects instead of real system access
   - Test pure logic: string parsing and conversion
   - MockPidStats provides test data without accessing /proc
   - No actual process manipulation occurs

4. **CpusetsInit tests** verify system capability detection
   - Test cgroup v2 detection logic
   - Check NUMA node configuration
   - Read-only operations that don't require root

5. **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)

6. **show_threads --cpuset help test** verifies CLI documentation
   - 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 (114 tests)

These tests require root to create/manipulate cgroups:

1. **Cpuset module tests** (32 tests) create actual cgroups in /sys/fs/cgroup
   - Test real cgroup v2 operations
   - Verify CPU assignment, memory node configuration, and CPU modification (add/remove)
   - Test task migration between cpusets
   - All tests clean up created cpusets in tearDown()

2. **Cpuset CLI tests** (51 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 (9 tests) - cpuset-based CPU isolation
   - Test --housekeeping-isolated flag to override default partition=member for housekeeping
   - Test tuna run --cpuset integration (5 tests) - running commands in cpusets
   - 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

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
- Testing with actual cgroups ensures real-world behavior

**Run without root:** Tests will be skipped with message "Requires root permissions"

## Writing New Tests

### Test File Structure

Create a new test file following this pattern:

```python
#!/usr/bin/env python3
"""
Description of what this test module tests.
"""

import unittest
import os
import sys


class TestSomething(unittest.TestCase):
    """Test cases for something in tuna"""

    @classmethod
    def setUpClass(cls):
        """Set up test fixtures (runs once before all tests)"""
        # Common setup for all tests in this class
        pass

    def setUp(self):
        """Set up for each test (runs before each test method)"""
        # Per-test setup
        pass

    def test_something(self):
        """Test that something works correctly"""
        # Your test code here
        self.assertEqual(expected, actual)
        self.assertTrue(condition)
        self.assertIn(item, collection)

    def tearDown(self):
        """Clean up after each test (runs after each test method)"""
        # Per-test cleanup
        pass


if __name__ == '__main__':
    unittest.main()
```

### Naming Conventions

- Test files: `test_*.py` (e.g., `test_affinity.py`)
- Test classes: `Test*` (e.g., `TestAffinityFunctions`)
- Test methods: `test_*` (e.g., `test_set_affinity_validates_input`)

### Common Assertions

```python
self.assertEqual(a, b)           # a == b
self.assertNotEqual(a, b)        # a != b
self.assertTrue(x)               # bool(x) is True
self.assertFalse(x)              # bool(x) is False
self.assertIn(a, b)              # a in b
self.assertNotIn(a, b)           # a not in b
self.assertIsNone(x)             # x is None
self.assertIsNotNone(x)          # x is not None
self.assertRaises(Exception, fn) # fn() raises Exception
```

## Requirements

### All Tests
- Python 3.6 or later
- python3-procfs package (for importing procfs module used by converters)
- No other external packages required (uses Python standard library and mocks)

### Root-Required Tests (cpuset tests only)
- Root permissions (sudo)
- cgroup v2 support (kernel 4.5+, recommended 5.0+)
- cgroup v2 mounted at /sys/fs/cgroup with cpuset controller enabled

## Expected Output

### Non-Root Tests Only (45 tests)

Running without sudo will run only the non-root tests:

```bash
$ python3 -m unittest discover -s tests -p "test_*.py" -v
test_get_affinity_eperm (test_eperm_handling.TestEPERMHandling) ... ok
test_get_priority_eperm (test_eperm_handling.TestEPERMHandling) ... ok
test_get_scheduler_eperm (test_eperm_handling.TestEPERMHandling) ... ok
test_set_affinity_eperm (test_eperm_handling.TestEPERMHandling) ... ok
test_set_priority_eperm (test_eperm_handling.TestEPERMHandling) ... ok
test_set_scheduler_eperm (test_eperm_handling.TestEPERMHandling) ... ok
test_duplicate_irqs_deduplicated (test_converters.TestIrqstringToList) ... ok
test_empty_string (test_converters.TestIrqstringToList) ... ok
test_multiple_numeric_irqs (test_converters.TestIrqstringToList) ... ok
test_single_numeric_irq (test_converters.TestIrqstringToList) ... ok
test_socketstring_exists (test_converters.TestSocketstringToList) ... ok
test_duplicate_pids_deduplicated (test_converters.TestThreadstringToList) ... ok
test_empty_string (test_converters.TestThreadstringToList) ... ok
test_exact_name_with_mock_ps (test_converters.TestThreadstringToList) ... ok
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
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 45 tests in 0.XXXs

OK (skipped=102)
```

### All Tests Including Root-Required (159 tests)

Running with sudo will run all 159 tests:

```bash
$ sudo python3 -m unittest discover -s tests -p "test_*.py" -v
... (45 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
... (51 cpuset CLI tests)
... (6 show_threads --cpuset tests)
... (4 show_threads -G display tests)

----------------------------------------------------------------------
Ran 159 tests in X.XXXs

OK
```

## Test Types

The test suite includes:

### 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 (114 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
- **Save/apply tests**: Test cpuset configuration save and restore functionality

Future test categories might include:
- **System tests** - Full end-to-end tests requiring CPU affinity operations
- **Regression tests** - Tests for specific bug fixes
- **Performance tests** - Benchmarking cpuset operations

## Continuous Integration

### Non-Root Tests (Recommended for CI)

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
- Fast execution (< 1 second)
- Includes basic cpuset capability detection and NUMA detection tests

```bash
# CI-friendly test command
python3 -m unittest discover -s tests -p "test_*.py" -v
```

### Root-Required Tests (Optional for CI)

The cpuset tests (114 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
- Slightly slower execution due to actual cgroup operations

```bash
# Full test suite (requires root)
sudo python3 -m unittest discover -s tests -p "test_*.py" -v
```
