Exit Status Testing
ParamikoMock supports mocking exit status for SSH commands, allowing you to test how your application handles different command execution outcomes. This is particularly useful for testing error handling and conditional logic based on command success or failure.
Basic Exit Status Usage
The SSHCommandMock class accepts an exit_status parameter to specify the exit status code for a command:
from paramiko_mock import (
SSHCommandMock, ParamikoMockEnviron,
SSHClientMock
)
from unittest.mock import patch
import paramiko
def test_exit_status_functionality():
# Set up mock responses with different exit statuses
responses_map = {
'ls -l': SSHCommandMock('', 'file1.txt\nfile2.txt', '', exit_status=0),
'cat missing_file.txt': SSHCommandMock('', '', 'cat: missing_file.txt: No such file or directory', exit_status=1),
'docker ps': SSHCommandMock('', '', 'Error: permission denied', exit_status=126)
},
ParamikoMockEnviron().add_responses_for_host(
host='test_host',
port=22,
responses=responses_map,
username='user',
password='pass'
)
def application_code():
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('test_host', port=22, username='user', password='pass')
# Test successful command
stdin, stdout, stderr = client.exec_command('ls -l')
exit_status = stderr.channel.recv_exit_status()
if exit_status == 0:
print(f"Success: {stdout.read().decode()}")
else:
print(f"Failed: {stderr.read().decode()}")
# Test failed command
stdin, stdout, stderr = client.exec_command('cat missing_file.txt')
exit_status = stderr.channel.recv_exit_status()
if exit_status == 0:
print(f"Success: {stdout.read().decode()}")
else:
print(f"Failed (status {exit_status}): {stderr.read().decode()}")
return exit_status
with patch('paramiko.SSHClient', new=SSHClientMock):
result = application_code()
# Verify the commands were executed
ParamikoMockEnviron().assert_command_was_executed('test_host', 22, 'ls -l')
ParamikoMockEnviron().assert_command_was_executed('test_host', 22, 'cat missing_file.txt')
ParamikoMockEnviron().cleanup_environment()
Common Exit Status Patterns
Here are some common exit status patterns you might want to test:
def test_common_exit_status_patterns():
ParamikoMockEnviron().add_responses_for_host('server', 22, {
# Success (0)
'success_command': SSHCommandMock('', 'Operation completed successfully', '', exit_status=0),
# General error (1)
'general_error': SSHCommandMock('', '', 'General error occurred', exit_status=1),
# Command not found (127)
'invalid_command': SSHCommandMock('', '', 'command not found', exit_status=127),
# Permission denied (126)
'permission_error': SSHCommandMock('', '', 'Permission denied', exit_status=126),
# Custom exit status
'custom_status': SSHCommandMock('', '', 'Custom application error', exit_status=42)
}, 'user', 'pass')
with patch('paramiko.SSHClient', new=SSHClientMock):
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('server', port=22, username='user', password='pass')
commands = ['success_command', 'general_error', 'invalid_command', 'permission_error', 'custom_status']
for cmd in commands:
stdin, stdout, stderr = client.exec_command(cmd)
exit_status = stderr.channel.recv_exit_status()
print(f"Command '{cmd}' exited with status: {exit_status}")
ParamikoMockEnviron().cleanup_environment()
Testing Error Handling Logic
Exit status mocking is particularly useful for testing error handling in your application:
def test_error_handling_logic():
ParamikoMockEnviron().add_responses_for_host('prod_server', 22, {
'backup_db': SSHCommandMock('', 'Database backed up successfully', '', exit_status=0),
'backup_db --fail': SSHCommandMock('', '', 'Backup failed: insufficient space', exit_status=1),
'check_service': SSHCommandMock('', 'Service is running', '', exit_status=0),
'check_service --down': SSHCommandMock('', '', 'Service is not running', exit_status=3)
}, 'admin', 'secret')
def backup_and_check():
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('prod_server', port=22, username='admin', password='secret')
# Try backup
stdin, stdout, stderr = client.exec_command('backup_db')
backup_status = stderr.channel.recv_exit_status()
if backup_status != 0:
print(f"Backup failed: {stderr.read().decode()}")
return False
# Check service status
stdin, stdout, stderr = client.exec_command('check_service')
service_status = stderr.channel.recv_exit_status()
if service_status != 0:
print(f"Service check failed: {stderr.read().decode()}")
return False
print("All operations completed successfully")
return True
with patch('paramiko.SSHClient', new=SSHClientMock):
# Test successful scenario
success = backup_and_check()
assert success == True
# Test failure scenario
ParamikoMockEnviron().add_responses_for_host('prod_server', 22, {
'backup_db': SSHCommandMock('', '', 'Backup failed: insufficient space', exit_status=1),
'check_service': SSHCommandMock('', 'Service is running', '', exit_status=0)
}, 'admin', 'secret')
success = backup_and_check()
assert success == False
ParamikoMockEnviron().cleanup_environment()
Exit Status with Regular Expressions
Exit status works with regular expressions just like other SSH command mocking:
def test_exit_status_with_regex():
ParamikoMockEnviron().add_responses_for_host('web_server', 22, {
're(httpd.*)': SSHCommandMock('', 'HTTP server running', '', exit_status=0),
're(fail.*)': SSHCommandMock('', '', 'Command execution failed', exit_status=1),
're(docker ps.*)': SSHCommandMock('', 'container1\ncontainer2', '', exit_status=0),
're(docker stop.*)': SSHCommandMock('', '', 'Error: container not found', exit_status=125)
}, 'user', 'pass')
with patch('paramiko.SSHClient', new=SSHClientMock):
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect('web_server', port=22, username='user', password='pass')
# These will match the regex patterns and return appropriate exit statuses
commands_to_test = [
'httpd status', # Matches 're(httpd.*)' -> exit_status 0
'fail_command', # Matches 're(fail.*)' -> exit_status 1
'docker ps -a', # Matches 're(docker ps.*)' -> exit_status 0
'docker stop missing' # Matches 're(docker stop.*)' -> exit_status 125
]
for cmd in commands_to_test:
stdin, stdout, stderr = client.exec_command(cmd)
exit_status = stderr.channel.recv_exit_status()
print(f"Command '{cmd}' -> exit status: {exit_status}")
ParamikoMockEnviron().cleanup_environment()
Complete Example
Here's a comprehensive example demonstrating exit status testing:
from paramiko_mock import (
SSHCommandMock, ParamikoMockEnviron,
SSHClientMock
)
from unittest.mock import patch
import paramiko
def example_application_function_with_exit_status():
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
'example_host',
port=22,
username='user',
password='pass'
)
# Execute a command that should succeed
stdin, stdout, stderr = client.exec_command('ls -l')
exit_status = stderr.channel.recv_exit_status()
if exit_status == 0:
print(f"Command succeeded with exit status {exit_status}")
print(f"Output: {stdout.read().decode()}")
else:
print(f"Command failed with exit status {exit_status}")
print(f"Error: {stderr.read().decode()}")
# Execute a command that should fail
stdin, stdout, stderr = client.exec_command('cat nonexistent_file.txt')
exit_status = stderr.channel.recv_exit_status()
if exit_status == 0:
print(f"Command succeeded with exit status {exit_status}")
print(f"Output: {stdout.read().decode()}")
else:
print(f"Command failed with exit status {exit_status}")
print(f"Error: {stderr.read().decode()}")
def test_example_application_function_with_exit_status():
# Set up mock responses with different exit statuses
ParamikoMockEnviron().add_responses_for_host('example_host', 22, {
'ls -l': SSHCommandMock('', 'file1.txt\nfile2.txt\ndir1/', '', exit_status=0),
'cat nonexistent_file.txt': SSHCommandMock('', '', 'cat: nonexistent_file.txt: No such file or directory', exit_status=1)
}, 'user', 'pass')
with patch('paramiko.SSHClient', new=SSHClientMock):
example_application_function_with_exit_status()
# Verify commands were executed
ParamikoMockEnviron().assert_command_was_executed('example_host', 22, 'ls -l')
ParamikoMockEnviron().assert_command_was_executed('example_host', 22, 'cat nonexistent_file.txt')
ParamikoMockEnviron().cleanup_environment()
Standard Exit Status Codes
Here are some common exit status codes you might want to test:
| Exit Status | Meaning | Common Use Case |
|---|---|---|
| 0 | Success | Command completed successfully |
| 1 | General error | Catch-all for various errors |
| 2 | Misuse of shell builtins | Incorrect command usage |
| 126 | Command cannot execute | Permission denied or command not executable |
| 127 | Command not found | Command doesn't exist |
| 128 | Invalid exit argument | Exit command with invalid argument |
| 128+n | Fatal error signal n | Command killed by signal |
| 130 | Script terminated by Control-C | User interrupted command |
| 255 | Exit status out of range | Exit code outside valid range |
Best Practices
-
Test both success and failure scenarios: Always test how your application handles both successful commands (exit status 0) and various failure scenarios.
-
Use realistic exit codes: Use exit status codes that match what the actual commands would return in production.
-
Test error message handling: Combine exit status testing with stderr output to ensure your application properly handles error messages.
-
Verify command execution: Use
assert_command_was_executed()to ensure the expected commands were actually called. -
Clean up environment: Always call
cleanup_environment()after each test to avoid interference between tests.
Exit status testing is essential for building robust applications that can handle command failures gracefully and provide appropriate error handling and user feedback.