# vim: set ft=python ts=4 sw=4 sts=4 et wrap: import argparse import logging import subprocess import tempfile from datetime import datetime from pathlib import Path from unittest import TestCase, mock import pytest from aeonview import ( AeonViewHelpers, AeonViewImages, AeonViewMessages, AeonViewVideos, ) # Define values used in the tests default_dest = str(Path.cwd() / "projects") default_project = "default" default_fps = 10 default_timeframe = "daily" default_simulate = False default_verbose = False default_image_domain = "https://example.com/image" default_test_path = Path("/tmp/test_project").resolve() tmp_images = Path("/tmp/images") # Define the Helpers class with methods to be tested class TestHelpers(TestCase): def test_check_date_valid(self): self.assertTrue(AeonViewHelpers.check_date(2023, 12, 31)) def test_check_date_invalid(self): self.assertFalse(AeonViewHelpers.check_date(2023, 2, 30)) def test_mkdir_p_creates_directory(self): with tempfile.TemporaryDirectory() as tmp: test_path = Path(tmp) / "a" / "b" / "c" AeonViewHelpers.mkdir_p(test_path) self.assertTrue(test_path.exists()) self.assertTrue(test_path.is_dir()) def test_get_extension_valid(self): prefix = default_image_domain types = ["jpg", "png", "gif", "webp"] for ext in types: self.assertEqual( AeonViewHelpers.get_extension(f"{prefix}.{ext}"), f".{ext}", ) def test_get_extension_invalid(self): self.assertEqual( AeonViewHelpers.get_extension(default_image_domain), ".jpg" ) # Default behavior self.assertIsNone(AeonViewHelpers.get_extension(None)) class TestFFmpegCommand(TestCase): def test_generate_ffmpeg_command(self): input_dir = tmp_images output_file = Path("/tmp/output.mp4") fps = 24 cmd = AeonViewHelpers.generate_ffmpeg_command( input_dir, output_file, fps ) self.assertIn("ffmpeg", cmd[0]) self.assertIn(str(fps), cmd) self.assertEqual(str(output_file), cmd[-1]) self.assertIn(str(input_dir / "*.{jpg,jpeg,png,gif,webp}"), cmd) def test_generate_ffmpeg_command_output_format(self): input_dir = tmp_images output_file = Path("/tmp/video.mp4") cmd = AeonViewHelpers.generate_ffmpeg_command( input_dir, output_file, 30 ) self.assertIn("/tmp/images/*.{jpg,jpeg,png,gif,webp}", cmd) self.assertIn("/tmp/video.mp4", cmd) self.assertIn("-c:v", cmd) self.assertIn("libx264", cmd) self.assertIn("-pix_fmt", cmd) self.assertIn("yuv420p", cmd) @mock.patch("subprocess.run") def test_simulate_ffmpeg_call(self, mock_run): input_dir = tmp_images output_file = Path("/tmp/out.mp4") cmd = AeonViewHelpers.generate_ffmpeg_command( input_dir, output_file, 10 ) subprocess.run(cmd, check=True) mock_run.assert_called_once_with(cmd) class TestAeonViewImages(TestCase): def setUp(self): self.args = argparse.Namespace() self.args.simulate = False self.args.date = "2025-04-10 12:30:45" self.args.url = f"{default_image_domain}.jpg" self.args.dest = default_test_path self.args.project = default_project self.args.verbose = default_verbose self.args.fps = default_fps self.args.timeframe = default_timeframe self.project_path = default_test_path self.url = f"{default_image_domain}.jpg" def test_get_image_paths_valid(self): url = f"{default_image_domain}.jpg" destination_base = default_test_path date = datetime(2025, 4, 10, 12, 30, 45) aeon_view_images = AeonViewImages(destination_base, url) paths = aeon_view_images.get_image_paths(url, destination_base, date) self.assertEqual(paths["url"], url) self.assertEqual(paths["file"], "12-30-45.jpg") self.assertEqual( paths["destinations"]["file"], destination_base / "2025-04" / "10" / "12-30-45.jpg", ) def test_get_image_paths_invalid_url(self): with pytest.raises(SystemExit): with self.assertLogs(level="ERROR") as log: aeon_view_images = AeonViewImages( default_test_path, "invalid-url" ) aeon_view_images.get_image_paths( "invalid-url", default_test_path, datetime(2025, 4, 10), ) self.assertIn(AeonViewMessages.INVALID_URL, log.output[0]) def test_get_image_paths_invalid_date(self): with pytest.raises(SystemExit): with self.assertLogs(level="ERROR") as log: aeon_view_images = AeonViewImages( default_test_path, f"{default_image_domain}.jpg" ) aeon_view_images.get_image_paths( f"{default_image_domain}.jpg", default_test_path, "invalid-date", ) self.assertIn(AeonViewMessages.INVALID_DATE, log.output[0]) @mock.patch("aeonview.AeonViewHelpers.mkdir_p") @mock.patch("aeonview.AeonViewImages.download_image") def test_get_current_image(self, mock_download_image, mock_mkdir_p): project_path = default_test_path url = f"{default_image_domain}.jpg" args = argparse.Namespace(simulate=False, date="2025-04-10 12:30:45") avi = AeonViewImages(project_path, url, args) avi.get_current_image() mock_mkdir_p.assert_called() mock_download_image.assert_called() @mock.patch("aeonview.requests.get") def test_download_image_success(self, mock_get): mock_response = mock.Mock() mock_response.status_code = 200 mock_response.iter_content = mock.Mock(return_value=[b"data"]) mock_get.return_value = mock_response project_path = default_test_path url = f"{default_image_domain}.jpg" args = argparse.Namespace(simulate=False) avi = AeonViewImages(project_path, url, args) with tempfile.NamedTemporaryFile(delete=True) as temp_file: destination = Path(temp_file.name) avi.download_image(destination) mock_get.assert_called_once_with(url, stream=True) self.assertTrue(destination.exists()) @mock.patch("aeonview.requests.get") def test_download_image_failure(self, mock_get): mock_response = mock.Mock() mock_response.status_code = 404 mock_get.return_value = mock_response project_path = default_test_path url = f"{default_image_domain}.jpg" args = argparse.Namespace(simulate=False) avi = AeonViewImages(project_path, url, args) destination = Path("/tmp/image.jpg") with pytest.raises(SystemExit): with self.assertLogs(level="ERROR") as log: avi.download_image(destination) self.assertIn(AeonViewMessages.DOWNLOAD_FAILURE, log.output[0]) class TestAeonViewVideos(TestCase): @mock.patch("aeonview.AeonViewHelpers.mkdir_p") @mock.patch("subprocess.run") def test_generate_daily_video(self, mock_subprocess_run, mock_mkdir_p): project_path = default_test_path args = argparse.Namespace( simulate=False, fps=10, day="01", month="04", year="2025" ) avv = AeonViewVideos(project_path, args) with self.assertLogs(level="INFO") as log: avv.generate_daily_video() msg = AeonViewMessages.VIDEO_GENERATION_SUCCESS expected_message = ( f"{msg}: {default_test_path / 'vid/2025-04/01.mp4'}" ) self.assertIn( expected_message, log.output[-1] ) # Ensure it's the last log entry mock_mkdir_p.assert_called() mock_subprocess_run.assert_called() @mock.patch("aeonview.AeonViewHelpers.mkdir_p") def test_generate_daily_video_simulate(self, mock_mkdir_p): project_path = default_test_path args = argparse.Namespace( simulate=True, fps=10, day="01", month="04", year="2025" ) avv = AeonViewVideos(project_path, args) avv.generate_daily_video() mock_mkdir_p.assert_not_called() def test_generate_monthly_video_not_implemented(self): project_path = default_test_path args = argparse.Namespace( simulate=False, fps=10, day="01", month="04", year="2025" ) avv = AeonViewVideos(project_path, args) with pytest.raises(NotImplementedError): avv.generate_monthly_video(Path("/tmp")) def test_generate_yearly_video_not_implemented(self): project_path = default_test_path args = argparse.Namespace( simulate=False, fps=10, day="01", month="04", year="2025" ) avv = AeonViewVideos(project_path, args) with pytest.raises(NotImplementedError): avv.generate_yearly_video(Path("/tmp")) class TestHelpersArguments(TestCase): def setUp(self): self.default_dest = str(Path.cwd() / "projects") @mock.patch( "sys.argv", [ "aeonview.py", "--mode", "image", "--url", f"{default_image_domain}.jpg", ], ) def test_parse_arguments_image_mode(self): args, _ = AeonViewHelpers.parse_arguments() self.assertEqual(args.mode, "image") self.assertEqual(args.url, f"{default_image_domain}.jpg") self.assertEqual(args.dest, self.default_dest) @mock.patch( "sys.argv", ["aeonview.py", "--mode", "video", "--project", f"{default_project}"], ) def test_parse_arguments_video_mode(self): args, _ = AeonViewHelpers.parse_arguments() self.assertEqual(args.mode, "video") self.assertEqual(args.project, f"{default_project}") self.assertEqual(args.dest, self.default_dest) @mock.patch("sys.argv", ["aeonview.py", "--mode", "image", "--simulate"]) def test_parse_arguments_simulate_mode(self): args, _ = AeonViewHelpers.parse_arguments() self.assertEqual(args.mode, "image") self.assertTrue(args.simulate) @mock.patch("sys.argv", ["aeonview.py", "--mode", "video", "--fps", "30"]) def test_parse_arguments_fps(self): args, _ = AeonViewHelpers.parse_arguments() self.assertEqual(args.mode, "video") self.assertEqual(args.project, f"{default_project}") self.assertEqual(args.dest, self.default_dest) self.assertEqual(args.fps, 30) @mock.patch( "sys.argv", ["aeonview.py", "--mode", "video", "--generate", "2023-10-01"], ) def test_parse_arguments_generate_date(self): args, _ = AeonViewHelpers.parse_arguments() self.assertEqual(args.mode, "video") self.assertEqual(args.generate, "2023-10-01") @mock.patch("sys.argv", ["aeonview.py", "--mode", "image", "--verbose"]) def test_parse_arguments_verbose(self): args, _ = AeonViewHelpers.parse_arguments() self.assertEqual(args.mode, "image") self.assertTrue(args.verbose) @mock.patch("sys.argv", ["aeonview.py"]) def test_parse_arguments_defaults(self): args, _ = AeonViewHelpers.parse_arguments() self.assertEqual(args.mode, "image") self.assertEqual(args.project, f"{default_project}") self.assertEqual(args.dest, self.default_dest) self.assertEqual(args.fps, 10) self.assertEqual(args.timeframe, "daily") self.assertFalse(args.simulate) self.assertFalse(args.verbose) class TestAeonViewSimulation(TestCase): @mock.patch("aeonview.AeonViewHelpers.mkdir_p") @mock.patch("aeonview.AeonViewImages.download_image") def test_image_simulation(self, mock_download_image, mock_mkdir_p): args = mock.MagicMock() args.simulate = True args.date = "2025-04-10 12:30:45" url = f"{default_image_domain}.jpg" project_path = Path("/tmp/test_project").resolve() avi = AeonViewImages(project_path, url, args) with mock.patch("aeonview.logging.info") as mock_logging: avi.get_current_image() mock_mkdir_p.assert_not_called() mock_download_image.assert_not_called() mock_logging.assert_any_call( f"Saving image to {project_path}/img/2025-04/10/12-30-45.jpg" ) @mock.patch("aeonview.AeonViewHelpers.mkdir_p") @mock.patch("subprocess.run") def test_video_simulation(self, mock_subprocess_run, mock_mkdir_p): args = mock.MagicMock() args.simulate = True args.fps = 10 args.day = "01" args.month = "01" args.year = "2023" project_path = Path("/tmp/test_project").resolve() avv = AeonViewVideos(project_path, args) with mock.patch("aeonview.logging.info") as mock_logging: avv.generate_daily_video() mock_mkdir_p.assert_not_called() mock_subprocess_run.assert_not_called() mock_logging.assert_any_call( f"Generating video from {project_path}/vid/2023-01/01" ) class TestSetupLogger(TestCase): @mock.patch("logging.basicConfig") def test_setup_logger_verbose(self, mock_basic_config): AeonViewHelpers.setup_logger(verbose=True) mock_basic_config.assert_called_once_with( level=logging.DEBUG, format="[%(levelname)s] %(message)s" ) @mock.patch("logging.basicConfig") def test_setup_logger_non_verbose(self, mock_basic_config): AeonViewHelpers.setup_logger(verbose=False) mock_basic_config.assert_called_once_with( level=logging.INFO, format="[%(levelname)s] %(message)s" ) mock_basic_config.reset_mock()