Files
aeonview/aeonview_test.py

319 lines
13 KiB
Python

import argparse
import logging
import subprocess
import tempfile
from pathlib import Path
from unittest import TestCase, mock
from datetime import datetime
import pytest
from aeonview import AeonViewHelpers, AeonViewImages, AeonViewVideos, AeonViewMessages
# 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()
# 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):
self.assertEqual(AeonViewHelpers.get_extension(f'{default_image_domain}.png'), '.png')
self.assertEqual(AeonViewHelpers.get_extension(f'{default_image_domain}.jpg'), '.jpg')
self.assertEqual(AeonViewHelpers.get_extension(f'{default_image_domain}.gif'), '.gif')
self.assertEqual(AeonViewHelpers.get_extension(f'{default_image_domain}.webp'), '.webp')
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 = Path('/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 = Path('/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 = Path('/tmp/images')
output_file = Path('/tmp/out.mp4')
cmd = AeonViewHelpers.generate_ffmpeg_command(input_dir, output_file, 10)
subprocess.run(cmd)
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)
paths = AeonViewImages.get_image_paths(self, 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:
AeonViewImages.get_image_paths(
self, '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:
AeonViewImages.get_image_paths(
self, 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)
destination = Path('/tmp/image.jpg')
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()
expected_message = f'{AeonViewMessages.VIDEO_GENERATION_SUCCESS}: {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()