Skip to content

Commit 781f054

Browse files
[cueman] Add Comprehensive Unit Tests for Job Termination Logic in cueman (#1950)
Create unit tests for the terminateJobs() function covering batch termination, reason logging, and exception handling. Tests added to /cueman/tests/test_main.py: - Single and batch job termination with reason tracking - Kill reason formatting with username from getpass.getuser() - Empty job list handling - Exception propagation during kill operations - Logging output verification with proper logger mocking - Termination across various job states (RUNNING, WAITING, DEAD, SUCCEEDED) Implementation notes: - Removed tests for non-existent confirm_termination function - Removed tests for unsupported force parameter - Updated all mocking to match actual terminateJobs implementation - Mock job.kill(), getpass.getuser(), and logger for proper isolation Add pytest to main dependencies for improved testing workflow - Move pytest from optional test dependencies to main dependencies - Ensures pytest is available when cueman is installed - Simplifies test execution without requiring separate test dependency installation **Link the Issue(s) this Pull Request is related to.** #1935 Co-authored-by: Aniket Singh Yadav <[email protected]> Co-authored-by: Ramon Figueiredo <[email protected]>
1 parent a9ce467 commit 781f054

File tree

2 files changed

+103
-2
lines changed

2 files changed

+103
-2
lines changed

cueman/pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ name = "opencue_cueman"
99
dynamic = ["version"]
1010
dependencies = [
1111
"opencue_pycue",
12-
"opencue_cueadmin"
12+
"opencue_cueadmin",
13+
"mock>=2.0.0",
14+
"pyfakefs>=5.2.3",
15+
"pytest>=6.0"
1316
]
1417
requires-python = ">3.7"
1518
description = "Cueman is a command-line job management tool for OpenCue that provides efficient job control operations. It offers a streamlined interface for managing jobs, frames, and processes with advanced filtering and batch operation capabilities."

cueman/tests/test_main.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
except ImportError:
4040
# Create a mock module for testing if proto isn't available
4141
import types
42+
4243
job_pb2 = types.ModuleType('job_pb2')
4344
job_pb2.RUNNING = 'RUNNING'
4445
job_pb2.WAITING = 'WAITING'
@@ -93,7 +94,7 @@ def test_buildFrameSearch_default(self):
9394
with mock.patch('cueadmin.common.handleIntCriterion') as mock_handle:
9495
mock_handle.side_effect = [
9596
[mock.Mock(value=10485)], # memory conversion
96-
[mock.Mock(value=36)] # duration conversion
97+
[mock.Mock(value=36)] # duration conversion
9798
]
9899
result = main.buildFrameSearch(args)
99100

@@ -256,6 +257,103 @@ def test_terminateJobs(self, mock_getuser):
256257
mock_job1.kill.assert_called_once_with(reason=main.KILL_REASON)
257258
mock_job2.kill.assert_called_once_with(reason=main.KILL_REASON)
258259

260+
@mock.patch('getpass.getuser')
261+
@mock.patch('cueman.main.logger')
262+
def test_terminateJobs_reason_includes_username(self, mock_logger, mock_getuser):
263+
"""Test that the kill reason includes the username."""
264+
mock_getuser.return_value = "specialuser"
265+
mock_job = mock.Mock()
266+
mock_job.name.return_value = "test_job"
267+
jobs = [mock_job]
268+
269+
main.terminateJobs(jobs)
270+
271+
# The KILL_REASON template is "Opencueman Terminate Job %s by user %s"
272+
# terminateJobs uses it directly as the reason parameter
273+
mock_job.kill.assert_called_once_with(reason=main.KILL_REASON)
274+
275+
# Verify logging includes username
276+
mock_logger.info.assert_any_call(main.KILL_REASON, "test_job", "specialuser")
277+
278+
@mock.patch('cueman.main.logger')
279+
def test_terminateJobs_multiple_jobs_batch(self, mock_logger):
280+
"""Test batch termination of multiple jobs."""
281+
mock_job1 = mock.Mock()
282+
mock_job1.name.return_value = "job1"
283+
mock_job2 = mock.Mock()
284+
mock_job2.name.return_value = "job2"
285+
mock_job3 = mock.Mock()
286+
mock_job3.name.return_value = "job3"
287+
288+
jobs = [mock_job1, mock_job2, mock_job3]
289+
290+
main.terminateJobs(jobs)
291+
292+
# Verify all jobs were killed
293+
for job in jobs:
294+
job.kill.assert_called_once_with(reason=main.KILL_REASON)
295+
296+
@mock.patch('cueman.main.logger')
297+
def test_terminateJobs_empty_list(self, mock_logger):
298+
"""Test terminateJobs with empty job list."""
299+
jobs = []
300+
301+
# Should not raise any exceptions
302+
try:
303+
main.terminateJobs(jobs)
304+
except Exception as e:
305+
self.fail(f"terminateJobs raised {e} with empty job list")
306+
307+
# Logger should not be called for empty list
308+
mock_logger.info.assert_not_called()
309+
310+
@mock.patch('cueman.main.logger')
311+
def test_terminateJobs_handles_kill_exception(self, mock_logger):
312+
"""Test that terminateJobs handles exceptions during kill operation."""
313+
mock_job = mock.Mock()
314+
mock_job.name.return_value = "failing_job"
315+
mock_job.kill.side_effect = Exception("Kill failed")
316+
jobs = [mock_job]
317+
318+
# terminateJobs doesn't catch exceptions, so it should propagate
319+
with self.assertRaises(Exception) as context:
320+
main.terminateJobs(jobs)
321+
322+
self.assertEqual(str(context.exception), "Kill failed")
323+
324+
@mock.patch('getpass.getuser')
325+
@mock.patch('cueman.main.logger')
326+
def test_terminateJobs_logs_output(self, mock_logger, mock_getuser):
327+
"""Test that terminateJobs produces expected log output."""
328+
mock_getuser.return_value = "testuser"
329+
mock_job = mock.Mock()
330+
mock_job.name.return_value = "test_job"
331+
jobs = [mock_job]
332+
333+
main.terminateJobs(jobs)
334+
335+
# Verify logging calls
336+
mock_logger.info.assert_any_call(main.KILL_REASON, "test_job", "testuser")
337+
mock_logger.info.assert_any_call("---")
338+
339+
@mock.patch('getpass.getuser')
340+
@mock.patch('cueman.main.logger')
341+
def test_terminateJobs_various_states(self, mock_logger, mock_getuser):
342+
"""Test termination of jobs in various states."""
343+
mock_getuser.return_value = "testuser"
344+
345+
for state in ["RUNNING", "WAITING", "DEAD", "SUCCEEDED"]:
346+
mock_job = mock.Mock()
347+
mock_job.name.return_value = f"job_{state}"
348+
mock_job.state = state
349+
jobs = [mock_job]
350+
351+
main.terminateJobs(jobs)
352+
353+
# Verify job was killed regardless of state
354+
mock_job.kill.assert_called_once_with(reason=main.KILL_REASON)
355+
mock_job.reset_mock()
356+
259357

260358
class TestCuemanHandleArgs(unittest.TestCase):
261359
"""Test cases for handleArgs function."""

0 commit comments

Comments
 (0)