Files
aeonview/aeonview_test.py

376 lines
14 KiB
Python

# 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()