]> git.feebdaed.xyz Git - 0xmirror/crun.git/commitdiff
tests: add command tests for pause, unpause, kill, list, ps, spec
authorGiuseppe Scrivano <gscrivan@redhat.com>
Wed, 17 Dec 2025 22:16:00 +0000 (22:16 +0000)
committerGiuseppe Scrivano <gscrivan@redhat.com>
Mon, 22 Dec 2025 07:10:55 +0000 (08:10 +0100)
Add test_commands.py with tests targeting CLI commands.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
Makefile.am
tests/test_commands.py [new file with mode: 0755]

index 87f9be8622889cfbf323fcbeb706f0a3ee1cbec2..e6d8ad631a650c90d0908fc4bfab14ceb38259f2 100644 (file)
@@ -267,6 +267,7 @@ PYTHON_TESTS = tests/test_capabilities.py \
        tests/test_seccomp.py \
        tests/test_time.py \
        tests/test_bpf_devices.py \
+       tests/test_commands.py \
        tests/test_net_device.py \
        tests/test_cgroup_resources.py
 
diff --git a/tests/test_commands.py b/tests/test_commands.py
new file mode 100755 (executable)
index 0000000..d53f045
--- /dev/null
@@ -0,0 +1,488 @@
+#!/bin/env python3
+# crun - OCI runtime written in C
+#
+# Copyright (C) 2017, 2018, 2019 Giuseppe Scrivano <giuseppe@scrivano.org>
+# crun is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# crun is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with crun.  If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import os
+import subprocess
+import tempfile
+import time
+from tests_utils import *
+
+def test_pause_unpause():
+    """Test pause and unpause commands."""
+    if is_rootless():
+        return (77, "requires root for cgroup freezer")
+
+    conf = base_config()
+    add_all_namespaces(conf, cgroupns=True)
+    conf['process']['args'] = ['/init', 'pause']
+
+    cid = None
+    try:
+        _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True)
+
+        # Verify container is running
+        state = json.loads(run_crun_command(['state', cid]))
+        if state['status'] != 'running':
+            logger.info("container not in running state: %s", state['status'])
+            return -1
+
+        # Pause the container
+        run_crun_command(['pause', cid])
+
+        # Verify container is paused
+        state = json.loads(run_crun_command(['state', cid]))
+        if state['status'] != 'paused':
+            logger.info("container not in paused state: %s", state['status'])
+            return -1
+
+        # Unpause the container
+        run_crun_command(['resume', cid])
+
+        # Verify container is running again
+        state = json.loads(run_crun_command(['state', cid]))
+        if state['status'] != 'running':
+            logger.info("container not in running state after resume: %s", state['status'])
+            return -1
+
+        return 0
+
+    except subprocess.CalledProcessError as e:
+        output = e.output.decode('utf-8', errors='ignore') if e.output else ''
+        if "freezer" in output.lower() or "cgroup" in output.lower():
+            return (77, "cgroup freezer not available")
+        logger.info("test failed: %s", e)
+        return -1
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+    finally:
+        if cid is not None:
+            run_crun_command(["delete", "-f", cid])
+
+
+def test_kill_signal():
+    """Test kill command with various signals."""
+
+    conf = base_config()
+    add_all_namespaces(conf)
+    conf['process']['args'] = ['/init', 'pause']
+
+    cid = None
+    try:
+        _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True)
+
+        # Verify container is running
+        state = json.loads(run_crun_command(['state', cid]))
+        if state['status'] != 'running':
+            logger.info("container not in running state: %s", state['status'])
+            return -1
+
+        # Send SIGKILL
+        run_crun_command(['kill', cid, 'SIGKILL'])
+
+        # Wait for container to stop
+        time.sleep(0.5)
+
+        # Verify container is stopped
+        state = json.loads(run_crun_command(['state', cid]))
+        if state['status'] != 'stopped':
+            logger.info("container not stopped after SIGKILL: %s", state['status'])
+            return -1
+
+        return 0
+
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+    finally:
+        if cid is not None:
+            run_crun_command(["delete", "-f", cid])
+
+
+def test_kill_signal_number():
+    """Test kill command with signal number."""
+
+    conf = base_config()
+    add_all_namespaces(conf)
+    conf['process']['args'] = ['/init', 'pause']
+
+    cid = None
+    try:
+        _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True)
+
+        # Send signal 9 (SIGKILL)
+        run_crun_command(['kill', cid, '9'])
+
+        # Wait for container to stop
+        time.sleep(0.5)
+
+        # Verify container is stopped
+        state = json.loads(run_crun_command(['state', cid]))
+        if state['status'] != 'stopped':
+            logger.info("container not stopped after signal 9: %s", state['status'])
+            return -1
+
+        return 0
+
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+    finally:
+        if cid is not None:
+            run_crun_command(["delete", "-f", cid])
+
+
+def test_list_containers():
+    """Test list command."""
+
+    conf = base_config()
+    add_all_namespaces(conf)
+    conf['process']['args'] = ['/init', 'pause']
+
+    cid1 = None
+    cid2 = None
+    try:
+        _, cid1 = run_and_get_output(conf, hide_stderr=True, command='run', detach=True)
+        _, cid2 = run_and_get_output(conf, hide_stderr=True, command='run', detach=True)
+
+        # List containers
+        output = run_crun_command(['list', '--format', 'json'])
+        containers = json.loads(output)
+
+        # Find our containers in the list
+        found_cid1 = False
+        found_cid2 = False
+        for c in containers:
+            if c.get('id') == cid1:
+                found_cid1 = True
+                if c.get('status') != 'running':
+                    logger.info("container %s not running: %s", cid1, c.get('status'))
+                    return -1
+            if c.get('id') == cid2:
+                found_cid2 = True
+                if c.get('status') != 'running':
+                    logger.info("container %s not running: %s", cid2, c.get('status'))
+                    return -1
+
+        if not found_cid1:
+            logger.info("container %s not found in list", cid1)
+            return -1
+        if not found_cid2:
+            logger.info("container %s not found in list", cid2)
+            return -1
+
+        return 0
+
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+    finally:
+        if cid1 is not None:
+            run_crun_command(["delete", "-f", cid1])
+        if cid2 is not None:
+            run_crun_command(["delete", "-f", cid2])
+
+
+
+
+def test_spec_generation():
+    """Test spec command generates valid OCI config."""
+    try:
+        with tempfile.TemporaryDirectory() as tmpdir:
+            # Generate spec
+            subprocess.check_call([get_crun_path(), 'spec'], cwd=tmpdir)
+
+            # Verify config.json was created
+            config_path = os.path.join(tmpdir, 'config.json')
+            if not os.path.exists(config_path):
+                logger.info("config.json not created")
+                return -1
+
+            # Verify it's valid JSON
+            with open(config_path) as f:
+                config = json.load(f)
+
+            # Verify basic OCI structure
+            if 'ociVersion' not in config:
+                logger.info("missing ociVersion")
+                return -1
+            if 'process' not in config:
+                logger.info("missing process")
+                return -1
+            if 'root' not in config:
+                logger.info("missing root")
+                return -1
+
+            return 0
+
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+
+
+def test_spec_rootless():
+    """Test spec command with --rootless flag."""
+    try:
+        with tempfile.TemporaryDirectory() as tmpdir:
+            # Generate rootless spec
+            subprocess.check_call([get_crun_path(), 'spec', '--rootless'], cwd=tmpdir)
+
+            # Verify config.json was created
+            config_path = os.path.join(tmpdir, 'config.json')
+            if not os.path.exists(config_path):
+                logger.info("config.json not created")
+                return -1
+
+            # Verify it's valid JSON
+            with open(config_path) as f:
+                config = json.load(f)
+
+            # Verify rootless configuration has user namespace
+            namespaces = config.get('linux', {}).get('namespaces', [])
+            has_user_ns = any(ns.get('type') == 'user' for ns in namespaces)
+            if not has_user_ns:
+                logger.info("rootless spec missing user namespace")
+                return -1
+
+            return 0
+
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+
+
+def test_kill_sigterm():
+    """Test kill command with SIGTERM."""
+
+    conf = base_config()
+    add_all_namespaces(conf)
+    conf['process']['args'] = ['/init', 'pause']
+
+    cid = None
+    try:
+        _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True)
+
+        # Send SIGTERM
+        run_crun_command(['kill', cid, 'SIGTERM'])
+
+        # Wait for container to stop
+        time.sleep(0.5)
+
+        return 0
+
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+    finally:
+        if cid is not None:
+            run_crun_command(["delete", "-f", cid])
+
+
+def test_kill_all():
+    """Test kill command with --all flag."""
+    if is_rootless():
+        return (77, "requires root for cgroup access")
+
+    conf = base_config()
+    add_all_namespaces(conf)
+    conf['process']['args'] = ['/init', 'pause']
+
+    cid = None
+    try:
+        _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True)
+
+        # Kill all processes in container
+        run_crun_command(['kill', '--all', cid, 'SIGKILL'])
+
+        # Wait for container to stop
+        time.sleep(0.5)
+
+        # Verify container is stopped
+        state = json.loads(run_crun_command(['state', cid]))
+        if state['status'] != 'stopped':
+            logger.info("container not stopped after kill --all: %s", state['status'])
+            return -1
+
+        return 0
+
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+    finally:
+        if cid is not None:
+            run_crun_command(["delete", "-f", cid])
+
+
+def test_list_table_format():
+    """Test list command with table format."""
+
+    conf = base_config()
+    add_all_namespaces(conf)
+    conf['process']['args'] = ['/init', 'pause']
+
+    cid = None
+    try:
+        _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True)
+
+        # List containers with table format (default)
+        output = run_crun_command(['list'])
+
+        # Table output should contain column headers
+        if 'ID' not in output and 'STATUS' not in output:
+            logger.info("table format missing headers: %s", output[:100])
+            return -1
+
+        # Should contain our container ID
+        if cid not in output:
+            logger.info("container %s not in list output", cid)
+            return -1
+
+        return 0
+
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+    finally:
+        if cid is not None:
+            run_crun_command(["delete", "-f", cid])
+
+
+def test_list_quiet():
+    """Test list command with quiet flag."""
+
+    conf = base_config()
+    add_all_namespaces(conf)
+    conf['process']['args'] = ['/init', 'pause']
+
+    cid = None
+    try:
+        _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True)
+
+        # List containers in quiet mode
+        output = run_crun_command(['list', '-q'])
+
+        # Quiet mode should just list container IDs
+        if cid not in output:
+            logger.info("container %s not in quiet list output", cid)
+            return -1
+
+        return 0
+
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+    finally:
+        if cid is not None:
+            run_crun_command(["delete", "-f", cid])
+
+
+def test_ps_table_format():
+    """Test ps command with table format."""
+    if is_rootless():
+        return (77, "requires root for cgroup access")
+
+    conf = base_config()
+    add_all_namespaces(conf)
+    conf['process']['args'] = ['/init', 'pause']
+
+    cid = None
+    try:
+        _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True)
+
+        # Get process list with table format (default)
+        output = run_crun_command(['ps', cid])
+
+        # Table output should contain PID header
+        if 'PID' not in output:
+            logger.info("ps table format missing PID header")
+            return -1
+
+        return 0
+
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+    finally:
+        if cid is not None:
+            run_crun_command(["delete", "-f", cid])
+
+
+def test_state_command():
+    """Test state command."""
+
+    conf = base_config()
+    add_all_namespaces(conf)
+    conf['process']['args'] = ['/init', 'pause']
+
+    cid = None
+    try:
+        _, cid = run_and_get_output(conf, hide_stderr=True, command='run', detach=True)
+
+        # Get container state
+        output = run_crun_command(['state', cid])
+        state = json.loads(output)
+
+        # Verify state structure
+        if 'ociVersion' not in state:
+            logger.info("state missing ociVersion")
+            return -1
+        if 'id' not in state:
+            logger.info("state missing id")
+            return -1
+        if state['id'] != cid:
+            logger.info("state id mismatch: expected %s, got %s", cid, state['id'])
+            return -1
+        if 'status' not in state:
+            logger.info("state missing status")
+            return -1
+        if 'pid' not in state:
+            logger.info("state missing pid")
+            return -1
+        if 'bundle' not in state:
+            logger.info("state missing bundle")
+            return -1
+
+        return 0
+
+    except Exception as e:
+        logger.info("test failed: %s", e)
+        return -1
+    finally:
+        if cid is not None:
+            run_crun_command(["delete", "-f", cid])
+
+
+
+
+all_tests = {
+    "pause-unpause": test_pause_unpause,
+    "kill-signal": test_kill_signal,
+    "kill-signal-number": test_kill_signal_number,
+    "kill-sigterm": test_kill_sigterm,
+    "kill-all": test_kill_all,
+    "list-containers": test_list_containers,
+    "list-table-format": test_list_table_format,
+    "list-quiet": test_list_quiet,
+    "ps-table-format": test_ps_table_format,
+    "spec-generation": test_spec_generation,
+    "spec-rootless": test_spec_rootless,
+    "state-command": test_state_command,
+}
+
+if __name__ == "__main__":
+    tests_main(all_tests)