Skip to content

Commit

Permalink
Add mocking-based unit test for the status command (#67)
Browse files Browse the repository at this point in the history
Also update `pyproject.toml` s.t. it contains settings for the `ruff` formatter and linter that are consistent with the GitHub action.
  • Loading branch information
SimonL22 authored Dec 23, 2024
1 parent 6b42e86 commit 6272c06
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 0 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Unit Tests

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
unit_tests:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["pypy3.9", "pypy3.10", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{matrix.python-version}}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install .
pip install pytest pytest-cov
- name: Test with pytest
run: |
pytest -v
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ package-data = { "qlever" = ["Qleverfiles/*"] }

[tool.pytest.ini_options]
pythonpath = ["src"]

[tool.ruff]
line-length = 79
[tool.ruff.lint]
extend-select = ["I"]
140 changes: 140 additions & 0 deletions test/qlever/commands/test_status_execute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import sys
import unittest
from io import StringIO
from unittest.mock import MagicMock, call, patch

import qlever.command
from qlever.commands.status import StatusCommand


def get_mock_args(only_show):
args = MagicMock()
args.cmdline_regex = "^(ServerMain|IndexBuilderMain)"
args.show = only_show
return [args, args.cmdline_regex, args.show]


class TestStatusCommand(unittest.TestCase):
@patch("qlever.commands.status.show_process_info")
@patch("psutil.process_iter")
# testing execute for 2 processes. Just the second one is a qlever process.
# Mocking the process_iter and show_process_info method and testing
# if the methods are called correctly.
def test_execute_processes_found(
self, mock_process_iter, mock_show_process_info
):
# Mocking the input for the execute function
[args, args.cmdline_regex, args.show] = get_mock_args(False)

# Creating mock psutil.Process objects with necessary attributes
mock_process1 = MagicMock()
mock_process1.as_dict.return_value = {"test": [1]}
# to test with real psutil.process objects use this:
"""mock_process1.as_dict.return_value = {
'cmdline': ['cmdline1'],
'pid': 1,
'username': 'user1',
'create_time': datetime.now().timestamp(),
'memory_info': MagicMock(rss=512 * 1024 * 1024) # 512 MB
}"""

mock_process2 = MagicMock()
mock_process2.as_dict.return_value = {"test": [2]}
# to test with real psutil.process objects use this:
"""mock_process2.as_dict.return_value = {
'cmdline': ['cmdline2'],
'pid': 2,
'username': 'user2',
'create_time': datetime.now().timestamp(),
'memory_info': MagicMock(rss=1024 * 1024 * 1024) # 1 GB
}"""

mock_process3 = MagicMock()
mock_process3.as_dict.return_value = {"test": [3]}

# Mock the return value of process_iter
# to be a list of these mocked process objects
mock_process_iter.return_value = [
mock_process1,
mock_process2,
mock_process3,
]

# Simulate show_process_info returning False for the first
# True for the second and False for the third process
mock_show_process_info.side_effect = [False, True, False]

sc = StatusCommand()

# Execute the function
result = sc.execute(args)

# Assert that process_iter was called once
mock_process_iter.assert_called_once()

# Assert that show_process_info was called 3times
# in correct order with the correct arguments
expected_calls = [
call(mock_process1, args.cmdline_regex, show_heading=True),
call(mock_process2, args.cmdline_regex, show_heading=True),
call(mock_process3, args.cmdline_regex, show_heading=False),
]
mock_show_process_info.assert_has_calls(
expected_calls, any_order=False
)
self.assertTrue(result)

@patch("qlever.util.show_process_info")
@patch("psutil.process_iter")
def test_execute_no_processes_found(
self, mock_process_iter, mock_show_process_info
):
# Mocking the input for the execute function
[args, args.cmdline_regex, args.show] = get_mock_args(False)

# Mock process_iter to return an empty list,
# simulating that no matching processes are found
mock_process_iter.return_value = []

# Capture the string-output
captured_output = StringIO()
sys.stdout = captured_output

# Instantiate the StatusCommand
status_command = StatusCommand()

# Execute the function
result = status_command.execute(args)

# Reset redirect
sys.stdout = sys.__stdout__

# Assert that process_iter was called once
mock_process_iter.assert_called_once()

# Assert that show_process_info was never called
# since there are no processes
mock_show_process_info.assert_not_called()

self.assertTrue(result)

# Verify the correct output was printed
self.assertIn("No processes found", captured_output.getvalue())

@patch.object(qlever.command.QleverCommand, "show")
def test_execute_show_action_description(self, mock_show):
# Mocking the input for the execute function
[args, args.cmdline_regex, args.show] = get_mock_args(True)

# Execute the function
result = StatusCommand().execute(args)

# Assert that verifies that show was called with the correct parameters
mock_show.assert_any_call(
f"Show all processes on this machine where "
f"the command line matches {args.cmdline_regex}"
f" using Python's psutil library",
only_show=args.show,
)

self.assertTrue(result)
41 changes: 41 additions & 0 deletions test/qlever/commands/test_status_other_methods.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import argparse
import unittest

from qlever.commands.status import StatusCommand


class TestStatusCommand(unittest.TestCase):
def test_description(self):
result = StatusCommand().description()
self.assertEqual(
result, "Show QLever processes running on this machine"
)

def test_should_have_qleverfile(self):
self.assertFalse(StatusCommand().should_have_qleverfile())

def test_relevant_qleverfile_arguments(self):
result = StatusCommand().relevant_qleverfile_arguments()
self.assertEqual(result, {})

def test_additional_arguments(self):
# Create an instance of StatusCommand
sc = StatusCommand()

# Create a parser and a subparser
parser = argparse.ArgumentParser()
subparser = parser.add_argument_group("test")
# Call the method
sc.additional_arguments(subparser)
# Parse an empty argument list to see the default
args = parser.parse_args([])

# Test that the default value is set correctly
self.assertEqual(args.cmdline_regex, "^(ServerMain|IndexBuilderMain)")

# Test that the help text is correctly set
argument_help = subparser._group_actions[-1].help
self.assertEqual(
argument_help,
"Show only processes where the command line matches this regex",
)

0 comments on commit 6272c06

Please sign in to comment.