mirror of
https://github.com/ivuorinen/aeonview.git
synced 2026-03-11 10:57:43 +00:00
chore(lint): fixed pyright errors, tests
This commit is contained in:
47
aeonview.py
47
aeonview.py
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
@@ -41,6 +43,7 @@ class AeonViewImages:
|
|||||||
self.project_path = project_path or None
|
self.project_path = project_path or None
|
||||||
self.url = url or None
|
self.url = url or None
|
||||||
self.args = args or {}
|
self.args = args or {}
|
||||||
|
self.simulate = getattr(args, "simulate", False)
|
||||||
|
|
||||||
def get_image_paths(
|
def get_image_paths(
|
||||||
self, url: str | None, destination_base: Path | None, date: datetime
|
self, url: str | None, destination_base: Path | None, date: datetime
|
||||||
@@ -86,7 +89,7 @@ class AeonViewImages:
|
|||||||
destination_file = AeonViewHelpers.build_path(destination, file_name)
|
destination_file = AeonViewHelpers.build_path(destination, file_name)
|
||||||
|
|
||||||
if not destination.exists():
|
if not destination.exists():
|
||||||
if self.args.get("simulate", False):
|
if getattr(self.args, "simulate", False):
|
||||||
logging.info("Simulate: would create %s", destination)
|
logging.info("Simulate: would create %s", destination)
|
||||||
else:
|
else:
|
||||||
AeonViewHelpers.mkdir_p(destination)
|
AeonViewHelpers.mkdir_p(destination)
|
||||||
@@ -116,7 +119,7 @@ class AeonViewImages:
|
|||||||
Download the image from the URL and save it to the project directory.
|
Download the image from the URL and save it to the project directory.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
date_param = self.args.get("date", None)
|
date_param = getattr(self.args, "date", None)
|
||||||
|
|
||||||
if date_param is not None:
|
if date_param is not None:
|
||||||
try:
|
try:
|
||||||
@@ -143,7 +146,7 @@ class AeonViewImages:
|
|||||||
|
|
||||||
logging.info("Saving image to %s", dest_file)
|
logging.info("Saving image to %s", dest_file)
|
||||||
|
|
||||||
if not self.args.get("simulate", False):
|
if not self.simulate:
|
||||||
AeonViewHelpers.mkdir_p(dest_dir)
|
AeonViewHelpers.mkdir_p(dest_dir)
|
||||||
self.download_image(dest_file)
|
self.download_image(dest_file)
|
||||||
else:
|
else:
|
||||||
@@ -167,10 +170,7 @@ class AeonViewImages:
|
|||||||
logging.error("Invalid destination path.")
|
logging.error("Invalid destination path.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if (
|
if not getattr(self.args, "simulate", False):
|
||||||
self.args.get("simulate", False) is False
|
|
||||||
or self.args.get("simulate", None) is None
|
|
||||||
):
|
|
||||||
logging.info("Downloading image from %s", self.url)
|
logging.info("Downloading image from %s", self.url)
|
||||||
response = requests.get(self.url, stream=True, timeout=10)
|
response = requests.get(self.url, stream=True, timeout=10)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@@ -210,14 +210,11 @@ class AeonViewVideos:
|
|||||||
self.project_path = project_path
|
self.project_path = project_path
|
||||||
self.args = args
|
self.args = args
|
||||||
|
|
||||||
self.args["simulate"] = (
|
self.simulate = getattr(args, "simulate", False)
|
||||||
args.get("simulate", False) if isinstance(args, dict) else False
|
self.fps = getattr(args, "fps", 10)
|
||||||
)
|
self.day = getattr(args, "day", None)
|
||||||
self.args["fps"] = args.get("fps", 10) if isinstance(args, dict) else 10
|
self.month = getattr(args, "month", None)
|
||||||
|
self.year = getattr(args, "year", None)
|
||||||
self.day = args.get("day", None) if args else None
|
|
||||||
self.month = args.get("month", None) if args else None
|
|
||||||
self.year = args.get("year", None) if args else None
|
|
||||||
|
|
||||||
self.path_images = AeonViewHelpers.build_path(self.project_path, "img")
|
self.path_images = AeonViewHelpers.build_path(self.project_path, "img")
|
||||||
self.path_videos = AeonViewHelpers.build_path(self.project_path, "vid")
|
self.path_videos = AeonViewHelpers.build_path(self.project_path, "vid")
|
||||||
@@ -235,13 +232,13 @@ class AeonViewVideos:
|
|||||||
output_dir = AeonViewHelpers.build_path(self.path_videos, year_month)
|
output_dir = AeonViewHelpers.build_path(self.path_videos, year_month)
|
||||||
output_file = AeonViewHelpers.build_path(output_dir, f"{self.day}.mp4")
|
output_file = AeonViewHelpers.build_path(output_dir, f"{self.day}.mp4")
|
||||||
ffmpeg_cmd = AeonViewHelpers.generate_ffmpeg_command(
|
ffmpeg_cmd = AeonViewHelpers.generate_ffmpeg_command(
|
||||||
input_dir, output_file, self.args.fps
|
input_dir, output_file, self.fps
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.info("Generating video from %s", input_dir)
|
logging.info("Generating video from %s", input_dir)
|
||||||
logging.info("Output file will be %s", output_file)
|
logging.info("Output file will be %s", output_file)
|
||||||
|
|
||||||
if not self.args.get("simulate", False):
|
if not self.simulate:
|
||||||
logging.info("Running ffmpeg command: %s", " ".join(ffmpeg_cmd))
|
logging.info("Running ffmpeg command: %s", " ".join(ffmpeg_cmd))
|
||||||
if not os.path.exists(input_dir):
|
if not os.path.exists(input_dir):
|
||||||
AeonViewHelpers.mkdir_p(output_dir)
|
AeonViewHelpers.mkdir_p(output_dir)
|
||||||
@@ -418,7 +415,15 @@ class AeonViewApp:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.args, self.parser = AeonViewHelpers.parse_arguments()
|
args, parser = AeonViewHelpers.parse_arguments()
|
||||||
|
self.args: argparse.Namespace = args
|
||||||
|
self.parser: argparse.ArgumentParser = parser
|
||||||
|
|
||||||
|
if self.args is None:
|
||||||
|
logging.error("No arguments provided.")
|
||||||
|
self.parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
AeonViewHelpers.setup_logger(self.args.verbose)
|
AeonViewHelpers.setup_logger(self.args.verbose)
|
||||||
self.base_path = Path(self.args.dest).resolve()
|
self.base_path = Path(self.args.dest).resolve()
|
||||||
|
|
||||||
@@ -480,6 +485,8 @@ class AeonViewApp:
|
|||||||
logging.error("Project path %s does not exist.", project_path)
|
logging.error("Project path %s does not exist.", project_path)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
generate_date = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
generate_date = (
|
generate_date = (
|
||||||
datetime.strptime(self.args.generate, "%Y-%m-%d")
|
datetime.strptime(self.args.generate, "%Y-%m-%d")
|
||||||
@@ -502,7 +509,9 @@ class AeonViewApp:
|
|||||||
logging.error("Invalid date: %s-%s-%s", year, month, day)
|
logging.error("Invalid date: %s-%s-%s", year, month, day)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
avm = AeonViewVideos(project_path, vars(self.args))
|
args: argparse.Namespace = self.args
|
||||||
|
|
||||||
|
avm = AeonViewVideos(project_path, args)
|
||||||
|
|
||||||
if self.args.timeframe == "daily":
|
if self.args.timeframe == "daily":
|
||||||
avm.generate_daily_video()
|
avm.generate_daily_video()
|
||||||
|
|||||||
575
aeonview_test.py
575
aeonview_test.py
@@ -5,7 +5,7 @@ import subprocess
|
|||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import TestCase, mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -16,7 +16,6 @@ from aeonview import (
|
|||||||
AeonViewVideos,
|
AeonViewVideos,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Define values used in the tests
|
|
||||||
default_dest = str(Path.cwd() / "projects")
|
default_dest = str(Path.cwd() / "projects")
|
||||||
default_project = "default"
|
default_project = "default"
|
||||||
default_fps = 10
|
default_fps = 10
|
||||||
@@ -28,348 +27,264 @@ default_test_path = Path("/tmp/test_project").resolve()
|
|||||||
tmp_images = Path("/tmp/images")
|
tmp_images = Path("/tmp/images")
|
||||||
|
|
||||||
|
|
||||||
# Define the Helpers class with methods to be tested
|
def test_check_date_valid():
|
||||||
class TestHelpers(TestCase):
|
assert AeonViewHelpers.check_date(2023, 12, 31)
|
||||||
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_check_date_invalid():
|
||||||
def test_generate_ffmpeg_command(self):
|
assert not AeonViewHelpers.check_date(2023, 2, 30)
|
||||||
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 test_mkdir_p_creates_directory():
|
||||||
def setUp(self):
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
self.args = argparse.Namespace()
|
test_path = Path(tmp) / "a" / "b" / "c"
|
||||||
self.args.simulate = False
|
AeonViewHelpers.mkdir_p(test_path)
|
||||||
self.args.date = "2025-04-10 12:30:45"
|
assert test_path.exists()
|
||||||
self.args.url = f"{default_image_domain}.jpg"
|
assert test_path.is_dir()
|
||||||
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):
|
@pytest.mark.parametrize("ext", ["jpg", "png", "gif", "webp"])
|
||||||
with pytest.raises(SystemExit):
|
def test_get_extension_valid(ext):
|
||||||
with self.assertLogs(level="ERROR") as log:
|
assert AeonViewHelpers.get_extension(f"{default_image_domain}.{ext}") == f".{ext}"
|
||||||
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")
|
def test_get_extension_invalid():
|
||||||
@mock.patch("aeonview.AeonViewImages.download_image")
|
assert AeonViewHelpers.get_extension(default_image_domain) == ".jpg"
|
||||||
def test_get_current_image(self, mock_download_image, mock_mkdir_p):
|
assert AeonViewHelpers.get_extension(None) is None
|
||||||
project_path = default_test_path
|
|
||||||
url = f"{default_image_domain}.jpg"
|
|
||||||
args = argparse.Namespace(simulate=False, date="2025-04-10 12:30:45")
|
def test_generate_ffmpeg_command():
|
||||||
avi = AeonViewImages(project_path, url, args)
|
input_dir = tmp_images
|
||||||
|
output_file = Path("/tmp/output.mp4")
|
||||||
|
fps = 24
|
||||||
|
cmd = AeonViewHelpers.generate_ffmpeg_command(input_dir, output_file, fps)
|
||||||
|
assert "ffmpeg" in cmd[0]
|
||||||
|
assert str(fps) in cmd
|
||||||
|
assert str(output_file) == cmd[-1]
|
||||||
|
assert str(input_dir / "*.{jpg,jpeg,png,gif,webp}") in cmd
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_ffmpeg_command_output_format():
|
||||||
|
input_dir = tmp_images
|
||||||
|
output_file = Path("/tmp/video.mp4")
|
||||||
|
cmd = AeonViewHelpers.generate_ffmpeg_command(input_dir, output_file, 30)
|
||||||
|
assert "/tmp/images/*.{jpg,jpeg,png,gif,webp}" in cmd
|
||||||
|
assert "/tmp/video.mp4" in cmd
|
||||||
|
assert "-c:v" in cmd
|
||||||
|
assert "libx264" in cmd
|
||||||
|
assert "-pix_fmt" in cmd
|
||||||
|
assert "yuv420p" in cmd
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("subprocess.run")
|
||||||
|
def test_simulate_ffmpeg_call(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, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_image_paths_valid():
|
||||||
|
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)
|
||||||
|
assert paths["url"] == url
|
||||||
|
assert paths["file"] == "12-30-45.jpg"
|
||||||
|
assert paths["destinations"]["file"] == destination_base / "2025-04" / "10" / "12-30-45.jpg"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_image_paths_invalid_url():
|
||||||
|
with pytest.raises(SystemExit), mock.patch("aeonview.logging.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))
|
||||||
|
assert AeonViewMessages.INVALID_URL in log.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_image_paths_invalid_date():
|
||||||
|
with pytest.raises(SystemExit), mock.patch("aeonview.logging.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") # pyright: ignore [reportArgumentType]
|
||||||
|
assert AeonViewMessages.INVALID_DATE in log.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("aeonview.AeonViewHelpers.mkdir_p")
|
||||||
|
@mock.patch("aeonview.AeonViewImages.download_image")
|
||||||
|
def test_get_current_image(mock_download_image, mock_mkdir_p):
|
||||||
|
args = argparse.Namespace(simulate=False, date="2025-04-10 12:30:45")
|
||||||
|
avi = AeonViewImages(default_test_path, f"{default_image_domain}.jpg", 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(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
|
||||||
|
|
||||||
|
args = argparse.Namespace(simulate=False)
|
||||||
|
avi = AeonViewImages(default_test_path, f"{default_image_domain}.jpg", args)
|
||||||
|
with tempfile.NamedTemporaryFile(delete=True) as temp_file:
|
||||||
|
destination = Path(temp_file.name)
|
||||||
|
avi.download_image(destination)
|
||||||
|
mock_get.assert_called_once_with(f"{default_image_domain}.jpg", stream=True, timeout=10)
|
||||||
|
assert destination.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("aeonview.requests.get")
|
||||||
|
def test_download_image_failure(mock_get):
|
||||||
|
mock_response = mock.Mock()
|
||||||
|
mock_response.status_code = 404
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
|
args = argparse.Namespace(simulate=False)
|
||||||
|
avi = AeonViewImages(default_test_path, f"{default_image_domain}.jpg", args)
|
||||||
|
destination = Path("/tmp/image.jpg")
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit), mock.patch("aeonview.logging.error") as log:
|
||||||
|
avi.download_image(destination)
|
||||||
|
assert AeonViewMessages.DOWNLOAD_FAILURE in log.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("aeonview.AeonViewHelpers.mkdir_p")
|
||||||
|
@mock.patch("subprocess.run")
|
||||||
|
def test_generate_daily_video(mock_subprocess_run, mock_mkdir_p):
|
||||||
|
args = argparse.Namespace(simulate=False, fps=10, day="01", month="04", year="2025")
|
||||||
|
avv = AeonViewVideos(default_test_path, args)
|
||||||
|
with mock.patch("aeonview.logging.info") as log:
|
||||||
|
avv.generate_daily_video()
|
||||||
|
last_call_args = log.call_args_list[-1][0]
|
||||||
|
assert last_call_args[0] == "%s: %s"
|
||||||
|
assert last_call_args[1] == AeonViewMessages.VIDEO_GENERATION_SUCCESS
|
||||||
|
assert last_call_args[2] == default_test_path / "vid/2025-04/01.mp4"
|
||||||
|
|
||||||
|
mock_mkdir_p.assert_called()
|
||||||
|
mock_subprocess_run.assert_called()
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("aeonview.AeonViewHelpers.mkdir_p")
|
||||||
|
def test_generate_daily_video_simulate(mock_mkdir_p):
|
||||||
|
args = argparse.Namespace(simulate=True, fps=10, day="01", month="04", year="2025")
|
||||||
|
avv = AeonViewVideos(default_test_path, args)
|
||||||
|
avv.generate_daily_video()
|
||||||
|
mock_mkdir_p.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_monthly_video_not_implemented():
|
||||||
|
args = argparse.Namespace(simulate=False, fps=10, day="01", month="04", year="2025")
|
||||||
|
avv = AeonViewVideos(default_test_path, args)
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
avv.generate_monthly_video(Path("/tmp"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_yearly_video_not_implemented():
|
||||||
|
args = argparse.Namespace(simulate=False, fps=10, day="01", month="04", year="2025")
|
||||||
|
avv = AeonViewVideos(default_test_path, args)
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
avv.generate_yearly_video(Path("/tmp"))
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("sys.argv", ["aeonview.py", "--mode", "image", "--url", f"{default_image_domain}.jpg"])
|
||||||
|
def test_parse_arguments_image_mode():
|
||||||
|
args, _ = AeonViewHelpers.parse_arguments()
|
||||||
|
assert args.mode == "image"
|
||||||
|
assert args.url == f"{default_image_domain}.jpg"
|
||||||
|
assert args.dest == default_dest
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("sys.argv", ["aeonview.py", "--mode", "video", "--project", f"{default_project}"])
|
||||||
|
def test_parse_arguments_video_mode():
|
||||||
|
args, _ = AeonViewHelpers.parse_arguments()
|
||||||
|
assert args.mode == "video"
|
||||||
|
assert args.project == default_project
|
||||||
|
assert args.dest == default_dest
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("sys.argv", ["aeonview.py", "--mode", "image", "--simulate"])
|
||||||
|
def test_parse_arguments_simulate_mode():
|
||||||
|
args, _ = AeonViewHelpers.parse_arguments()
|
||||||
|
assert args.mode == "image"
|
||||||
|
assert args.simulate
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("sys.argv", ["aeonview.py", "--mode", "video", "--fps", "30"])
|
||||||
|
def test_parse_arguments_fps():
|
||||||
|
args, _ = AeonViewHelpers.parse_arguments()
|
||||||
|
assert args.mode == "video"
|
||||||
|
assert args.project == default_project
|
||||||
|
assert args.dest == default_dest
|
||||||
|
assert args.fps == 30
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("sys.argv", ["aeonview.py", "--mode", "video", "--generate", "2023-10-01"])
|
||||||
|
def test_parse_arguments_generate_date():
|
||||||
|
args, _ = AeonViewHelpers.parse_arguments()
|
||||||
|
assert args.mode == "video"
|
||||||
|
assert args.generate == "2023-10-01"
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("sys.argv", ["aeonview.py", "--mode", "image", "--verbose"])
|
||||||
|
def test_parse_arguments_verbose():
|
||||||
|
args, _ = AeonViewHelpers.parse_arguments()
|
||||||
|
assert args.mode == "image"
|
||||||
|
assert args.verbose
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("sys.argv", ["aeonview.py"])
|
||||||
|
def test_parse_arguments_defaults():
|
||||||
|
args, _ = AeonViewHelpers.parse_arguments()
|
||||||
|
assert args.mode == "image"
|
||||||
|
assert args.project == default_project
|
||||||
|
assert args.dest == default_dest
|
||||||
|
assert args.fps == 10
|
||||||
|
assert args.timeframe == "daily"
|
||||||
|
assert not args.simulate
|
||||||
|
assert not args.verbose
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("aeonview.AeonViewHelpers.mkdir_p")
|
||||||
|
@mock.patch("aeonview.AeonViewImages.download_image")
|
||||||
|
def test_image_simulation(mock_download_image, mock_mkdir_p):
|
||||||
|
args = mock.MagicMock()
|
||||||
|
args.simulate = True
|
||||||
|
args.date = "2025-04-10 12:30:45"
|
||||||
|
avi = AeonViewImages(default_test_path, f"{default_image_domain}.jpg", args)
|
||||||
|
with mock.patch("aeonview.logging.info") as log:
|
||||||
avi.get_current_image()
|
avi.get_current_image()
|
||||||
mock_mkdir_p.assert_called()
|
mock_mkdir_p.assert_not_called()
|
||||||
mock_download_image.assert_called()
|
mock_download_image.assert_not_called()
|
||||||
|
assert any("Saving image to" in str(call) for call in log.call_args_list)
|
||||||
@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("aeonview.AeonViewHelpers.mkdir_p")
|
@mock.patch("subprocess.run")
|
||||||
@mock.patch("subprocess.run")
|
def test_video_simulation(mock_subprocess_run, mock_mkdir_p):
|
||||||
def test_generate_daily_video(self, mock_subprocess_run, mock_mkdir_p):
|
args = mock.MagicMock()
|
||||||
project_path = default_test_path
|
args.simulate = True
|
||||||
args = argparse.Namespace(
|
args.fps = 10
|
||||||
simulate=False, fps=10, day="01", month="04", year="2025"
|
args.day = "01"
|
||||||
)
|
args.month = "01"
|
||||||
avv = AeonViewVideos(project_path, args)
|
args.year = "2023"
|
||||||
with self.assertLogs(level="INFO") as log:
|
avv = AeonViewVideos(default_test_path, args)
|
||||||
avv.generate_daily_video()
|
with mock.patch("aeonview.logging.info") as log:
|
||||||
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()
|
avv.generate_daily_video()
|
||||||
mock_mkdir_p.assert_not_called()
|
mock_mkdir_p.assert_not_called()
|
||||||
|
mock_subprocess_run.assert_not_called()
|
||||||
def test_generate_monthly_video_not_implemented(self):
|
assert any("Generating video from" in str(call) for call in log.call_args_list)
|
||||||
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):
|
@mock.patch("logging.basicConfig")
|
||||||
def setUp(self):
|
def test_setup_logger_verbose(mock_basic_config):
|
||||||
self.default_dest = str(Path.cwd() / "projects")
|
AeonViewHelpers.setup_logger(verbose=True)
|
||||||
|
mock_basic_config.assert_called_once_with(level=logging.DEBUG, format="[%(levelname)s] %(message)s")
|
||||||
@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("logging.basicConfig")
|
||||||
@mock.patch("aeonview.AeonViewHelpers.mkdir_p")
|
def test_setup_logger_non_verbose(mock_basic_config):
|
||||||
@mock.patch("aeonview.AeonViewImages.download_image")
|
AeonViewHelpers.setup_logger(verbose=False)
|
||||||
def test_image_simulation(self, mock_download_image, mock_mkdir_p):
|
mock_basic_config.assert_called_once_with(level=logging.INFO, format="[%(levelname)s] %(message)s")
|
||||||
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()
|
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ pre-commit>=3.5.0
|
|||||||
# Testing
|
# Testing
|
||||||
pytest>=8.0.0
|
pytest>=8.0.0
|
||||||
pytest-cov>=4.1.0
|
pytest-cov>=4.1.0
|
||||||
|
|
||||||
# Linting & formatting
|
# Linting & formatting
|
||||||
ruff>=0.3.3
|
ruff>=0.3.3
|
||||||
|
|||||||
Reference in New Issue
Block a user