| 
 | 1 | +import tempfile  | 
 | 2 | +import os  | 
 | 3 | +from contextlib import suppress  | 
 | 4 | +from io import StringIO  | 
 | 5 | +from unittest import TestCase  | 
 | 6 | +from unittest.mock import patch  | 
 | 7 | + | 
 | 8 | +from django.core.management import call_command  | 
 | 9 | +from django.core.management.base import CommandError  | 
 | 10 | +from rq.cron import CronJob  | 
 | 11 | + | 
 | 12 | +from ..cron import DjangoCronScheduler  | 
 | 13 | +from ..management.commands.rqcron import Command as RqcronCommand  | 
 | 14 | +from .fixtures import say_hello  | 
 | 15 | + | 
 | 16 | + | 
 | 17 | +class CronTest(TestCase):  | 
 | 18 | + | 
 | 19 | +    def test_django_cron_scheduler_init(self):  | 
 | 20 | +        """Test DjangoCronScheduler can be initialized without connection."""  | 
 | 21 | +        scheduler = DjangoCronScheduler()  | 
 | 22 | + | 
 | 23 | +        # Should not have connection until first register() call  | 
 | 24 | +        self.assertIsNone(scheduler.connection)  | 
 | 25 | +        self.assertIsNone(scheduler._connection_config)  | 
 | 26 | +        self.assertEqual(scheduler._cron_jobs, [])  | 
 | 27 | + | 
 | 28 | +    def test_first_register_initializes_connection(self):  | 
 | 29 | +        """Test that first register() call initializes the scheduler with queue's connection."""  | 
 | 30 | +        scheduler = DjangoCronScheduler()  | 
 | 31 | + | 
 | 32 | +        # Register a job with cron expression (run every minute)  | 
 | 33 | +        cron_job = scheduler.register(say_hello, 'default', cron='* * * * *')  | 
 | 34 | + | 
 | 35 | +        # Should now have connection set  | 
 | 36 | +        self.assertIsNotNone(scheduler.connection)  | 
 | 37 | +        self.assertIsNotNone(scheduler._connection_config)  | 
 | 38 | +        self.assertIsInstance(cron_job, CronJob)  | 
 | 39 | +        self.assertEqual(len(scheduler.get_jobs()), 1)  | 
 | 40 | + | 
 | 41 | +        # Verify cron expression is set correctly  | 
 | 42 | +        self.assertEqual(cron_job.cron, '* * * * *')  | 
 | 43 | +        self.assertIsNone(cron_job.interval)  | 
 | 44 | +        self.assertIsNotNone(cron_job.next_run_time)  | 
 | 45 | + | 
 | 46 | +    def test_connection_validation(self):  | 
 | 47 | +        """Test connection validation for same, compatible, and incompatible queues."""  | 
 | 48 | +        # Start with test3 queue (localhost:6379, DB=1)  | 
 | 49 | +        scheduler = DjangoCronScheduler()  | 
 | 50 | + | 
 | 51 | +        # Same queue multiple times should work  | 
 | 52 | +        job1 = scheduler.register(say_hello, 'test3', interval=60)  | 
 | 53 | +        job2 = scheduler.register(say_hello, 'test3', interval=120)  | 
 | 54 | + | 
 | 55 | +        self.assertEqual(len(scheduler.get_jobs()), 2)  | 
 | 56 | +        self.assertEqual(job1.queue_name, 'test3')  | 
 | 57 | +        self.assertEqual(job2.queue_name, 'test3')  | 
 | 58 | + | 
 | 59 | +        # Compatible queues (same Redis connection) should work  | 
 | 60 | +        # Both 'test3' and 'async' use localhost:6379 with DB=1  | 
 | 61 | +        job3 = scheduler.register(say_hello, 'async', interval=180)  | 
 | 62 | +        self.assertEqual(len(scheduler.get_jobs()), 3)  | 
 | 63 | +        self.assertEqual(job3.queue_name, 'async')  | 
 | 64 | + | 
 | 65 | +        # Queues having different Redis connections should fail  | 
 | 66 | +        # 'default' uses DB=0 while test3/async use DB=1  | 
 | 67 | +        with self.assertRaises(ValueError):  | 
 | 68 | +            scheduler.register(say_hello, 'default', interval=240)  | 
 | 69 | + | 
 | 70 | +        # Undefined queue_name should fail  | 
 | 71 | +        scheduler = DjangoCronScheduler()  | 
 | 72 | +        with self.assertRaises(KeyError):  | 
 | 73 | +            scheduler.register(say_hello, 'nonexistent_queue', interval=300)  | 
 | 74 | + | 
 | 75 | + | 
 | 76 | +class CronCommandTest(TestCase):  | 
 | 77 | + | 
 | 78 | +    @patch('django_rq.cron.DjangoCronScheduler.start')  | 
 | 79 | +    def test_rqcron_command(self, mock_start):  | 
 | 80 | +        """Test rqcron command execution: success and import errors from load_config_from_file."""  | 
 | 81 | +        mock_start.return_value = None  | 
 | 82 | + | 
 | 83 | +        # Test 1: Successful execution  | 
 | 84 | +        out = StringIO()  | 
 | 85 | +        config_path = 'django_rq.tests.cron_config1'  | 
 | 86 | + | 
 | 87 | +        call_command('rqcron', config_path, stdout=out)  | 
 | 88 | + | 
 | 89 | +        output = out.getvalue()  | 
 | 90 | +        self.assertIn(f'Loading cron configuration from {config_path}', output)  | 
 | 91 | +        self.assertIn('Starting cron scheduler with 2 jobs...', output)  | 
 | 92 | +        mock_start.assert_called_once()  | 
 | 93 | + | 
 | 94 | +        # Test 2: File not found - should raise ImportError from RQ  | 
 | 95 | +        with self.assertRaises(ImportError) as cm:  | 
 | 96 | +            call_command('rqcron', 'nonexistent_file.py')  | 
 | 97 | + | 
 | 98 | +        self.assertIn("No module named 'nonexistent_file'", str(cm.exception))  | 
 | 99 | + | 
 | 100 | +        # Test 3: Import error  | 
 | 101 | +        with self.assertRaises(ImportError) as cm:  | 
 | 102 | +            call_command('rqcron', 'nonexistent.module.path')  | 
 | 103 | + | 
 | 104 | +        self.assertIn("No module named 'nonexistent'", str(cm.exception))  | 
 | 105 | + | 
 | 106 | +    @patch('django_rq.cron.DjangoCronScheduler.start')  | 
 | 107 | +    @patch('django_rq.cron.DjangoCronScheduler.load_config_from_file')  | 
 | 108 | +    def test_rqcron_command_exceptions(self, mock_load_config, mock_start):  | 
 | 109 | +        """Test rqcron command exception handling."""  | 
 | 110 | +        mock_load_config.return_value = None  | 
 | 111 | + | 
 | 112 | +        # Test KeyboardInterrupt handling  | 
 | 113 | +        mock_start.side_effect = KeyboardInterrupt()  | 
 | 114 | +        with self.assertRaises(SystemExit):  | 
 | 115 | +            call_command('rqcron', 'django_rq.tests.cron_config2')  | 
 | 116 | + | 
 | 117 | +        # Test general exception handling - should bubble up as raw exception  | 
 | 118 | +        mock_load_config.side_effect = Exception("Test error")  | 
 | 119 | +        with self.assertRaises(Exception) as cm:  | 
 | 120 | +            call_command('rqcron', 'django_rq.tests.cron_config2')  | 
 | 121 | + | 
 | 122 | +        self.assertEqual(str(cm.exception), "Test error")  | 
 | 123 | + | 
 | 124 | +    def test_rqcron_command_successful_run(self):  | 
 | 125 | +        """Test successful rqcron command execution without mocking."""  | 
 | 126 | +        out = StringIO()  | 
 | 127 | +        config_path = 'django_rq.tests.cron_config1'  | 
 | 128 | + | 
 | 129 | +        # Use a very short timeout to test actual execution  | 
 | 130 | +        import signal  | 
 | 131 | + | 
 | 132 | +        def timeout_handler(signum, frame):  | 
 | 133 | +            raise KeyboardInterrupt()  | 
 | 134 | + | 
 | 135 | +        # Set up a timeout to stop the command after a short time  | 
 | 136 | +        old_handler = signal.signal(signal.SIGALRM, timeout_handler)  | 
 | 137 | +        signal.alarm(1)  # Stop after 1 second  | 
 | 138 | + | 
 | 139 | +        try:  | 
 | 140 | +            # The command will be interrupted and may or may not raise SystemExit depending on Django version  | 
 | 141 | +            with suppress(SystemExit):  | 
 | 142 | +                call_command('rqcron', config_path, stdout=out)  | 
 | 143 | +        finally:  | 
 | 144 | +            signal.alarm(0)  # Cancel the alarm  | 
 | 145 | +            signal.signal(signal.SIGALRM, old_handler)  | 
 | 146 | + | 
 | 147 | +        output = out.getvalue()  | 
 | 148 | +        self.assertIn(f'Loading cron configuration from {config_path}', output)  | 
 | 149 | +        self.assertIn('Starting cron scheduler with 2 jobs...', output)  | 
0 commit comments