From a2217c4a4a90012ddd410e81b2263b54af13e55b Mon Sep 17 00:00:00 2001 From: Luis Eduardo Salazar Date: Sun, 30 Nov 2025 18:40:15 -0500 Subject: [PATCH 1/6] Enhance main.py with SHA384 compatibility and update server configuration; add requirements.txt for dependencies --- main.py | 15 ++++++++++++--- requirements.txt | 5 +++++ 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 requirements.txt diff --git a/main.py b/main.py index 4d256a6..405de15 100644 --- a/main.py +++ b/main.py @@ -3,8 +3,18 @@ import cherrypy import ffmpeg from base64 import b64encode -from Crypto.Hash import SHA384 from io import BytesIO +try: + from Crypto.Hash import SHA384 +except ImportError: + try: + from Cryptodome.Hash import SHA384 + except ImportError: + import hashlib + class SHA384: + @staticmethod + def new(data): + return hashlib.sha384(data) from mimetypes import guess_type from os import mkdir, access, listdir, R_OK, W_OK, X_OK from os.path import abspath, exists, isdir, basename, getsize @@ -225,7 +235,6 @@ def main(): elif not access(VID_FOLDER, R_OK | W_OK | X_OK): perr(f'ERROR: Insufficient privileges on {VID_FOLDER}') exit(2) - # vid folder OK. Continue # Check static folders if not exists(JS_FOLDER) or not isdir(JS_FOLDER): perr(f'ERROR: Missing {JS_FOLDER} folder.') @@ -233,7 +242,6 @@ def main(): if not exists(CSS_FOLDER) or not isdir(CSS_FOLDER): perr(f'ERROR: Missing {CSS_FOLDER} folder.') exit(4) - # Statics OK. Continue # Configure and launch app app : Viewer = Viewer() app_config : dict = dict() @@ -248,6 +256,7 @@ def main(): app_config['/vid']['tools.staticdir.dir'] = abspath(VID_FOLDER) cherrypy.tree.mount(app, '/', app_config) + cherrypy.config.update({'server.socket_host': '0.0.0.0'}) cherrypy.engine.subscribe('stop', app.stop) cherrypy.engine.start() cherrypy.engine.block() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6ee4612 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +cherrypy>=18.0.0 +ffmpeg-python>=1.0.0 +Pillow>=10.0.0 +yattag>=1.0.0 +pycryptodomex>=3.0.0 From c224a44c11cf2ba0fc4fe4ce7f706926225e19e4 Mon Sep 17 00:00:00 2001 From: Luis Eduardo Salazar Date: Sun, 30 Nov 2025 18:51:44 -0500 Subject: [PATCH 2/6] Add testing framework setup with pytest configuration, test dependencies, and CI workflow; include initial test cases for utility functions and main application logic --- .github/workflows/ci.yml | 82 ++++++ pytest.ini | 9 + requirements-test.txt | 3 + tests/README.md | 105 +++++++ tests/__init__.py | 2 + tests/conftest.py | 89 ++++++ tests/test_helpers.py | 50 ++++ tests/test_main.py | 581 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 921 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 pytest.ini create mode 100644 requirements-test.txt create mode 100644 tests/README.md create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_helpers.py create mode 100644 tests/test_main.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b7f5524 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,82 @@ +name: CI + +on: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements-test.txt + + - name: Run tests + run: | + pytest + + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-test.txt + pip install flake8 + + - name: Run flake8 + run: | + flake8 main.py __main__.py tests/ --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 main.py __main__.py tests/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + codeql-analysis: + name: CodeQL Analysis + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..d44684c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,9 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = -v --tb=short +markers = + slow: marks tests as slow (deselect with '-m "not slow"') + diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..90293e5 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,3 @@ +pytest>=7.0.0 +pytest-cov>=4.0.0 + diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..beac0bd --- /dev/null +++ b/tests/README.md @@ -0,0 +1,105 @@ +# Test Suite for vview + +This directory contains unit tests for the vview video viewer application. + +## Running Tests + +### Prerequisites + +Install test dependencies: + +```bash +pip install -r requirements-test.txt +``` + +Or install pytest directly: + +```bash +pip install pytest pytest-cov +``` + +### Running All Tests + +```bash +pytest +``` + +### Running with Verbose Output + +```bash +pytest -v +``` + +### Running Specific Test Files + +```bash +pytest tests/test_main.py +pytest tests/test_helpers.py +``` + +### Running Specific Test Classes + +```bash +pytest tests/test_main.py::TestViewerInit +pytest tests/test_main.py::TestIndex +``` + +### Running Specific Tests + +```bash +pytest tests/test_main.py::TestViewerInit::test_init_creates_hashes_for_css_js +pytest tests/test_helpers.py::TestPerr::test_perr_writes_to_stderr +``` + +### Running with Coverage + +```bash +pytest --cov=main --cov-report=html +``` + +This will generate an HTML coverage report in the `htmlcov/` directory. + +## Test Structure + +### `test_main.py` +Tests for the `Viewer` class and its methods: +- `TestViewerInit` - Tests for Viewer initialization +- `TestUpdateThumbnails` - Tests for thumbnail update functionality +- `TestIndex` - Tests for the index page generation +- `TestRefresh` - Tests for the refresh endpoint +- `TestVvid` - Tests for the video viewing page +- `TestStop` - Tests for the stop handler + +### `test_helpers.py` +Tests for utility functions: +- `TestPerr` - Tests for the `perr()` error logging function + +### `conftest.py` +Shared pytest fixtures for test setup: +- `temp_dir` - Temporary directory for test files +- `mock_css_folder` - Mock CSS folder with test files +- `mock_js_folder` - Mock JS folder with test files +- `mock_vid_folder` - Mock video folder +- `mock_db_path` - Temporary database path +- `mock_ffmpeg_probe` - Mock ffmpeg.probe response +- `mock_ffmpeg_output` - Mock ffmpeg output data +- `sample_video_files` - Sample video files for testing + +## Test Coverage + +The test suite covers: +- Viewer class initialization and hash generation +- Thumbnail database operations (create, update, delete) +- HTML page generation (index, video player) +- Error handling and edge cases +- Utility functions + +## Notes + +- Tests use mocking extensively to avoid dependencies on: + - File system operations + - Database connections + - FFmpeg operations + - CherryPy server +- All external dependencies are mocked to ensure tests run quickly and reliably + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..0601f78 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# Tests package for vview + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..9591cea --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,89 @@ +"""Pytest configuration and shared fixtures for vview tests.""" + +import os +import shutil +import sqlite3 +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, Mock, patch +import pytest + + +@pytest.fixture +def temp_dir(): + """Create a temporary directory for testing.""" + temp_path = tempfile.mkdtemp() + yield temp_path + shutil.rmtree(temp_path, ignore_errors=True) + + +@pytest.fixture +def mock_css_folder(temp_dir): + """Create a mock CSS folder with test files.""" + css_dir = os.path.join(temp_dir, 'css') + os.makedirs(css_dir, exist_ok=True) + css_file = os.path.join(css_dir, 'bootstrap.min.css') + with open(css_file, 'w') as f: + f.write('/* mock css */') + return css_dir + + +@pytest.fixture +def mock_js_folder(temp_dir): + """Create a mock JS folder with test files.""" + js_dir = os.path.join(temp_dir, 'js') + os.makedirs(js_dir, exist_ok=True) + js_file = os.path.join(js_dir, 'bootstrap.bundle.min.js') + with open(js_file, 'w') as f: + f.write('// mock js') + return js_dir + + +@pytest.fixture +def mock_vid_folder(temp_dir): + """Create a mock video folder.""" + vid_dir = os.path.join(temp_dir, 'vid') + os.makedirs(vid_dir, exist_ok=True) + return vid_dir + + +@pytest.fixture +def mock_db_path(temp_dir): + """Create a path for a temporary database.""" + return os.path.join(temp_dir, 'test_metadata.sqlite') + + +@pytest.fixture +def mock_ffmpeg_probe(): + """Mock ffmpeg.probe response.""" + return { + 'streams': [ + { + 'codec_type': 'video', + 'width': '1920', + 'height': '1080', + 'r_frame_rate': '30/1' + } + ] + } + + +@pytest.fixture +def mock_ffmpeg_output(): + """Mock ffmpeg output (raw frame data).""" + # Create a mock RGB24 frame (1920x1080x3 bytes) + width, height = 1920, 1080 + frame_size = width * height * 3 + return (b'\x00' * frame_size, b'') + + +@pytest.fixture +def sample_video_files(mock_vid_folder): + """Create sample video files in the mock video folder.""" + files = ['test1.mp4', 'test2.mp4', 'other.txt'] + for fname in files: + filepath = os.path.join(mock_vid_folder, fname) + with open(filepath, 'w') as f: + f.write('mock video content') + return files + diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..60b0222 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,50 @@ +"""Tests for utility functions in main.py.""" + +import sys +from io import StringIO +from unittest.mock import patch, MagicMock +import pytest + +# Import the function to test +sys.path.insert(0, '/home/lsalab/.prn/vview') +from main import perr + + +class TestPerr: + """Tests for the perr() utility function.""" + + def test_perr_writes_to_stderr(self): + """Test that perr writes messages to stderr.""" + test_message = "Test error message" + mock_stderr = StringIO() + with patch('main.stderr', mock_stderr): + perr(test_message) + assert test_message in mock_stderr.getvalue() + + def test_perr_strips_newlines(self): + """Test that perr strips newlines from input.""" + test_message = "Test message\nwith\r\nnewlines" + mock_stderr = StringIO() + with patch('main.stderr', mock_stderr): + perr(test_message) + output = mock_stderr.getvalue() + # Should not contain the original newlines, but should have one at the end + assert '\r\n' in output + assert test_message.strip('\r\n') in output + + def test_perr_adds_newline(self): + """Test that perr adds a newline at the end.""" + test_message = "Test message" + mock_stderr = StringIO() + with patch('main.stderr', mock_stderr): + perr(test_message) + output = mock_stderr.getvalue() + assert output.endswith('\r\n') + + def test_perr_flushes_stderr(self): + """Test that perr flushes stderr.""" + mock_stderr = MagicMock() + with patch('main.stderr', mock_stderr): + perr("Test message") + mock_stderr.flush.assert_called_once() + diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..53a789d --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,581 @@ +"""Tests for the Viewer class and main functionality.""" + +import os +import sqlite3 +from unittest.mock import MagicMock, Mock, patch, mock_open +import pytest +import sys + +# Add the project root to the path +sys.path.insert(0, '/home/lsalab/.prn/vview') + +from main import Viewer, THUMBNAIL_DB, THUMBNAIL_HEIGHT, THUMBNAIL_TIME + + +class TestViewerInit: + """Tests for Viewer.__init__ method.""" + + @patch('main.connect') + @patch('main.listdir') + @patch('main.abspath') + @patch('main.basename') + @patch('builtins.open', new_callable=mock_open, read_data=b'test content') + @patch('main.SHA384') + def test_init_creates_hashes_for_css_js( + self, mock_sha384, mock_file, mock_basename, mock_abspath, + mock_listdir, mock_connect + ): + """Test that __init__ creates hashes for CSS and JS files.""" + # Setup mocks + mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + mock_abspath.side_effect = lambda x: x + mock_basename.side_effect = lambda x: os.path.basename(x) + + mock_hash = MagicMock() + mock_hash.digest.return_value = b'test_digest' + mock_sha384.new.return_value = mock_hash + + mock_con = MagicMock() + mock_cur = MagicMock() + mock_cur.execute.return_value.fetchall.return_value = [] + mock_con.cursor.return_value = mock_cur + mock_connect.return_value.__enter__.return_value = mock_con + + with patch.object(Viewer, '_update_thumbnails'): + viewer = Viewer() + assert 'css' in viewer.hashes + assert 'js' in viewer.hashes + + @patch('main.connect') + @patch('main.listdir') + @patch('main.abspath') + @patch('main.basename') + @patch('builtins.open', new_callable=mock_open, read_data=b'test content') + @patch('main.SHA384') + def test_init_creates_database_table_if_not_exists( + self, mock_sha384, mock_file, mock_basename, mock_abspath, + mock_listdir, mock_connect + ): + """Test that __init__ creates the thumbnails table if it doesn't exist.""" + # Setup mocks + mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + mock_abspath.side_effect = lambda x: x + mock_basename.side_effect = lambda x: os.path.basename(x) + + mock_hash = MagicMock() + mock_hash.digest.return_value = b'test_digest' + mock_sha384.new.return_value = mock_hash + + mock_con = MagicMock() + mock_cur = MagicMock() + # First call: check for table existence (returns empty) + # Second call: create table + mock_cur.execute.return_value.fetchall.return_value = [] + mock_con.cursor.return_value = mock_cur + mock_connect.return_value.__enter__.return_value = mock_con + + with patch.object(Viewer, '_update_thumbnails'): + viewer = Viewer() + # Verify that execute was called (for table creation check) + assert mock_cur.execute.called + + +class TestUpdateThumbnails: + """Tests for Viewer._update_thumbnails method.""" + + @patch('main.ffmpeg.probe') + @patch('main.ffmpeg.input') + @patch('main.Image') + @patch('main.BytesIO') + @patch('main.connect') + @patch('main.listdir') + @patch('main.abspath') + @patch('main.basename') + @patch('main.connect') + @patch('main.listdir') + @patch('main.abspath') + @patch('main.basename') + @patch('builtins.open', new_callable=mock_open, read_data=b'test content') + @patch('main.SHA384') + def test_update_thumbnails_removes_deleted_files( + self, mock_sha384, mock_file, mock_basename, mock_abspath, mock_listdir, mock_connect, + mock_bytesio, mock_image, mock_ffmpeg_input, mock_ffmpeg_probe + ): + """Test that _update_thumbnails removes metadata for deleted files.""" + # Setup basic mocks for Viewer init + mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + mock_abspath.side_effect = lambda x: x + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_hash = MagicMock() + mock_hash.digest.return_value = b'test_digest' + mock_sha384.new.return_value = mock_hash + + mock_con_init = MagicMock() + mock_cur_init = MagicMock() + mock_cur_init.execute.return_value.fetchall.return_value = [] + mock_con_init.cursor.return_value = mock_cur_init + mock_connect.return_value.__enter__.return_value = mock_con_init + + viewer = Viewer() + viewer.hashes = {'css': {}, 'js': {}} + + # Setup mocks + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_abspath.side_effect = lambda x: x + mock_listdir.return_value = ['test1.mp4'] # Only one file exists + + mock_con = MagicMock() + mock_cur = MagicMock() + # Database has 'deleted.mp4' which doesn't exist in filesystem + mock_cur.execute.return_value.fetchall.side_effect = [ + [('deleted.mp4',)], # Existing metadata + [] # After deletion + ] + mock_con.cursor.return_value = mock_cur + mock_connect.return_value.__enter__.return_value = mock_con + + viewer._update_thumbnails() + + # Verify DELETE was called + delete_calls = [call for call in mock_cur.execute.call_args_list + if 'DELETE' in str(call)] + assert len(delete_calls) > 0 + + @patch('main.ffmpeg.probe') + @patch('main.ffmpeg.input') + @patch('main.Image') + @patch('main.BytesIO') + @patch('main.connect') + @patch('main.listdir') + @patch('main.abspath') + @patch('main.basename') + @patch('builtins.open', new_callable=mock_open, read_data=b'test content') + @patch('main.SHA384') + def test_update_thumbnails_creates_metadata_for_new_files( + self, mock_sha384, mock_file, mock_basename, mock_abspath, mock_listdir, mock_connect, + mock_bytesio, mock_image, mock_ffmpeg_input, mock_ffmpeg_probe + ): + """Test that _update_thumbnails creates metadata for new MP4 files.""" + # Setup basic mocks for Viewer init + mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + mock_abspath.side_effect = lambda x: x + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_hash = MagicMock() + mock_hash.digest.return_value = b'test_digest' + mock_sha384.new.return_value = mock_hash + + mock_con_init = MagicMock() + mock_cur_init = MagicMock() + mock_cur_init.execute.return_value.fetchall.return_value = [] + mock_con_init.cursor.return_value = mock_cur_init + mock_connect.return_value.__enter__.return_value = mock_con_init + + viewer = Viewer() + viewer.hashes = {'css': {}, 'js': {}} + + # Setup mocks + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_abspath.side_effect = lambda x: x + mock_listdir.return_value = ['new_video.mp4'] + + # Mock ffmpeg probe + mock_ffmpeg_probe.return_value = { + 'streams': [{ + 'codec_type': 'video', + 'width': '1920', + 'height': '1080', + 'r_frame_rate': '30/1' + }] + } + + # Mock ffmpeg input/output + mock_output = MagicMock() + mock_output.run.return_value = (b'\x00' * (1920 * 1080 * 3), b'') + mock_ffmpeg_input.return_value.filter.return_value.output.return_value = mock_output + + # Mock PIL Image + mock_img = MagicMock() + mock_img.resize.return_value = mock_img + mock_image.frombytes.return_value = mock_img + + # Mock BytesIO + mock_io = MagicMock() + mock_io.getbuffer.return_value = b'image_data' + mock_bytesio.return_value.__enter__.return_value = mock_io + + mock_con = MagicMock() + mock_cur = MagicMock() + # No existing metadata + mock_cur.execute.return_value.fetchall.return_value = [] + mock_con.cursor.return_value = mock_cur + mock_connect.return_value.__enter__.return_value = mock_con + + viewer._update_thumbnails() + + # Verify INSERT was called + insert_calls = [call for call in mock_cur.execute.call_args_list + if 'INSERT' in str(call)] + assert len(insert_calls) > 0 + + @patch('main.connect') + @patch('main.listdir') + @patch('main.abspath') + @patch('main.basename') + @patch('builtins.open', new_callable=mock_open, read_data=b'test content') + @patch('main.SHA384') + def test_update_thumbnails_ignores_non_mp4_files( + self, mock_sha384, mock_file, mock_basename, mock_abspath, mock_listdir, mock_connect + ): + """Test that _update_thumbnails ignores non-MP4 files.""" + # Setup basic mocks for Viewer init + mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + mock_abspath.side_effect = lambda x: x + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_hash = MagicMock() + mock_hash.digest.return_value = b'test_digest' + mock_sha384.new.return_value = mock_hash + + mock_con_init = MagicMock() + mock_cur_init = MagicMock() + mock_cur_init.execute.return_value.fetchall.return_value = [] + mock_con_init.cursor.return_value = mock_cur_init + mock_connect.return_value.__enter__.return_value = mock_con_init + + viewer = Viewer() + viewer.hashes = {'css': {}, 'js': {}} + + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_abspath.side_effect = lambda x: x + mock_listdir.return_value = ['video.txt', 'video.avi'] + + mock_con = MagicMock() + mock_cur = MagicMock() + mock_cur.execute.return_value.fetchall.return_value = [] + mock_con.cursor.return_value = mock_cur + mock_connect.return_value.__enter__.return_value = mock_con + + viewer._update_thumbnails() + + # Should not call ffmpeg for non-MP4 files + # (This is implicit - if it tried, it would fail without mocks) + + +class TestIndex: + """Tests for Viewer.index method.""" + + @patch('main.connect') + @patch('main.listdir') + @patch('main.abspath') + @patch('main.basename') + @patch('builtins.open', new_callable=mock_open, read_data=b'test content') + @patch('main.SHA384') + def test_index_returns_html( + self, mock_sha384, mock_file, mock_basename, mock_abspath, mock_listdir, mock_connect + ): + """Test that index() returns valid HTML.""" + # Setup basic mocks for Viewer init + mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + mock_abspath.side_effect = lambda x: x + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_hash = MagicMock() + mock_hash.digest.return_value = b'test_digest' + mock_sha384.new.return_value = mock_hash + + mock_con_init = MagicMock() + mock_cur_init = MagicMock() + mock_cur_init.execute.return_value.fetchall.return_value = [] + mock_con_init.cursor.return_value = mock_cur_init + mock_connect.return_value.__enter__.return_value = mock_con_init + + with patch.object(Viewer, '_update_thumbnails'): + viewer = Viewer() + + viewer.hashes = { + 'css': {'bootstrap.min.css': 'test_hash'}, + 'js': {'bootstrap.bundle.min.js': 'test_hash'} + } + + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_abspath.side_effect = lambda x: x + mock_listdir.return_value = [] + + mock_con = MagicMock() + mock_cur = MagicMock() + mock_cur.execute.return_value.fetchone.return_value = None + mock_con.cursor.return_value = mock_cur + mock_connect.return_value.__enter__.return_value = mock_con + + result = viewer.index() + + assert isinstance(result, str) + assert '' in result + assert '' in result + assert 'Video viewer' in result + + @patch('main.connect') + @patch('main.listdir') + @patch('main.abspath') + @patch('main.basename') + @patch('builtins.open', new_callable=mock_open, read_data=b'test content') + @patch('main.SHA384') + def test_index_displays_videos( + self, mock_sha384, mock_file, mock_basename, mock_abspath, mock_listdir, mock_connect + ): + """Test that index() displays video thumbnails.""" + # Setup basic mocks for Viewer init + mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + mock_abspath.side_effect = lambda x: x + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_hash = MagicMock() + mock_hash.digest.return_value = b'test_digest' + mock_sha384.new.return_value = mock_hash + + mock_con_init = MagicMock() + mock_cur_init = MagicMock() + mock_cur_init.execute.return_value.fetchall.return_value = [] + mock_con_init.cursor.return_value = mock_cur_init + mock_connect.return_value.__enter__.return_value = mock_con_init + + with patch.object(Viewer, '_update_thumbnails'): + viewer = Viewer() + + viewer.hashes = { + 'css': {'bootstrap.min.css': 'test_hash'}, + 'js': {'bootstrap.bundle.min.js': 'test_hash'} + } + + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_abspath.side_effect = lambda x: x + mock_listdir.return_value = ['test1.mp4', 'test2.mp4'] + + mock_con = MagicMock() + mock_cur = MagicMock() + mock_cur.execute.return_value.fetchone.return_value = ('base64_image_data',) + mock_con.cursor.return_value = mock_cur + mock_connect.return_value.__enter__.return_value = mock_con + + result = viewer.index() + + assert 'test1.mp4' in result or 'test2.mp4' in result + assert 'data:image/png;base64' in result + + +class TestRefresh: + """Tests for Viewer.refresh method.""" + + @patch('main.cherrypy.HTTPRedirect') + @patch('main.connect') + @patch('main.listdir') + @patch('main.abspath') + @patch('main.basename') + @patch('builtins.open', new_callable=mock_open, read_data=b'test content') + @patch('main.SHA384') + def test_refresh_calls_update_thumbnails( + self, mock_sha384, mock_file, mock_basename, mock_abspath, + mock_listdir, mock_connect, mock_update, mock_redirect + ): + """Test that refresh() calls _update_thumbnails.""" + # Setup basic mocks for Viewer init + mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + mock_abspath.side_effect = lambda x: x + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_hash = MagicMock() + mock_hash.digest.return_value = b'test_digest' + mock_sha384.new.return_value = mock_hash + + mock_con_init = MagicMock() + mock_cur_init = MagicMock() + mock_cur_init.execute.return_value.fetchall.return_value = [] + mock_con_init.cursor.return_value = mock_cur_init + mock_connect.return_value.__enter__.return_value = mock_con_init + + with patch.object(Viewer, '_update_thumbnails') as mock_update: + viewer = Viewer() + + viewer.hashes = {'css': {}, 'js': {}} + + mock_redirect_instance = MagicMock() + mock_redirect.return_value = mock_redirect_instance + + try: + viewer.refresh() + except: + pass # HTTPRedirect raises an exception + + mock_update.assert_called_once() + + +class TestVvid: + """Tests for Viewer.vvid method.""" + + @patch('main.listdir') + @patch('main.abspath') + @patch('main.basename') + @patch('main.connect') + @patch('builtins.open', new_callable=mock_open, read_data=b'test content') + @patch('main.SHA384') + def test_vvid_returns_html_for_valid_video( + self, mock_sha384, mock_file, mock_connect, mock_basename, mock_abspath, mock_listdir + ): + """Test that vvid() returns HTML for a valid video.""" + # Setup basic mocks for Viewer init + mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + mock_abspath.side_effect = lambda x: x + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_hash = MagicMock() + mock_hash.digest.return_value = b'test_digest' + mock_sha384.new.return_value = mock_hash + + mock_con_init = MagicMock() + mock_cur_init = MagicMock() + mock_cur_init.execute.return_value.fetchall.return_value = [] + mock_con_init.cursor.return_value = mock_cur_init + mock_connect.return_value.__enter__.return_value = mock_con_init + + with patch.object(Viewer, '_update_thumbnails'): + viewer = Viewer() + + viewer.hashes = { + 'css': { + 'bootstrap.min.css': 'test_hash', + 'video-js.css': 'test_hash' + }, + 'js': { + 'bootstrap.bundle.min.js': 'test_hash', + 'video.min.js': 'test_hash', + 'videojs.hotkeys.min.js': 'test_hash' + } + } + + mock_basename.side_effect = lambda x: os.path.basename(x) + mock_abspath.side_effect = lambda x: x + mock_listdir.return_value = ['test_video.mp4'] + + result = viewer.vvid('test_video.mp4') + + assert isinstance(result, str) + assert '' in result + assert ' Date: Sun, 30 Nov 2025 19:00:00 -0500 Subject: [PATCH 3/6] Add pyproject.toml for project metadata and dependencies; update requirements.txt for ffmpeg-python version; modify CI workflow to restrict Python versions and install system dependencies --- .github/workflows/ci.yml | 7 ++++++- pyproject.toml | 33 +++++++++++++++++++++++++++++++++ requirements.txt | 3 ++- 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 pyproject.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7f5524..3014b44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 @@ -19,6 +19,11 @@ jobs: python-version: ${{ matrix.python-version }} cache: 'pip' + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y ffmpeg + - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9c2ee69 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "vview" +version = "1.0.0" +description = "Video viewer web application with thumbnail generation" +requires-python = ">=3.11" +dependencies = [ + "cherrypy>=18.0.0", + "ffmpeg-python>=0.2.0", + "Pillow>=10.0.0", + "yattag>=1.0.0", + "pycryptodomex>=3.0.0", +] + +[project.optional-dependencies] +test = [ + "pytest>=7.0.0", + "pytest-cov>=4.0.0", +] + +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", +] + diff --git a/requirements.txt b/requirements.txt index 6ee4612..68e772e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ +# Python >= 3.11 required cherrypy>=18.0.0 -ffmpeg-python>=1.0.0 +ffmpeg-python>=0.2.0 Pillow>=10.0.0 yattag>=1.0.0 pycryptodomex>=3.0.0 From 92459ccaeaa1e4d29fab1808fd453740c6c39506 Mon Sep 17 00:00:00 2001 From: Luis Eduardo Salazar Date: Sun, 30 Nov 2025 19:03:19 -0500 Subject: [PATCH 4/6] Update pytest configuration to include pythonpath; clean up test files by removing hardcoded paths --- pyproject.toml | 1 + pytest.ini | 1 + tests/test_helpers.py | 2 -- tests/test_main.py | 4 ---- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9c2ee69..873a63f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] addopts = "-v --tb=short" +pythonpath = ["."] markers = [ "slow: marks tests as slow (deselect with '-m \"not slow\"')", ] diff --git a/pytest.ini b/pytest.ini index d44684c..b8ca962 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,6 +4,7 @@ python_files = test_*.py python_classes = Test* python_functions = test_* addopts = -v --tb=short +pythonpath = . markers = slow: marks tests as slow (deselect with '-m "not slow"') diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 60b0222..5e61a8d 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,12 +1,10 @@ """Tests for utility functions in main.py.""" -import sys from io import StringIO from unittest.mock import patch, MagicMock import pytest # Import the function to test -sys.path.insert(0, '/home/lsalab/.prn/vview') from main import perr diff --git a/tests/test_main.py b/tests/test_main.py index 53a789d..ad67554 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -4,10 +4,6 @@ import sqlite3 from unittest.mock import MagicMock, Mock, patch, mock_open import pytest -import sys - -# Add the project root to the path -sys.path.insert(0, '/home/lsalab/.prn/vview') from main import Viewer, THUMBNAIL_DB, THUMBNAIL_HEIGHT, THUMBNAIL_TIME From 8e9cd1e4ec5a97613d1ed31e87d8f3f3142a0e57 Mon Sep 17 00:00:00 2001 From: Luis Eduardo Salazar Date: Sun, 30 Nov 2025 19:09:02 -0500 Subject: [PATCH 5/6] Refactor tests in test_main.py to improve mocking and enhance test coverage for Viewer._update_thumbnails method. Consolidate ffmpeg and BytesIO mocks, streamline listdir side effects, and ensure proper database connection handling in tests. --- tests/test_main.py | 237 +++++++++++++++++++++++++++++++-------------- 1 file changed, 164 insertions(+), 73 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index ad67554..51c4e38 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -79,33 +79,59 @@ def test_init_creates_database_table_if_not_exists( class TestUpdateThumbnails: """Tests for Viewer._update_thumbnails method.""" - @patch('main.ffmpeg.probe') - @patch('main.ffmpeg.input') + @patch('main.ffmpeg') @patch('main.Image') @patch('main.BytesIO') @patch('main.connect') @patch('main.listdir') @patch('main.abspath') @patch('main.basename') - @patch('main.connect') - @patch('main.listdir') - @patch('main.abspath') - @patch('main.basename') @patch('builtins.open', new_callable=mock_open, read_data=b'test content') @patch('main.SHA384') def test_update_thumbnails_removes_deleted_files( self, mock_sha384, mock_file, mock_basename, mock_abspath, mock_listdir, mock_connect, - mock_bytesio, mock_image, mock_ffmpeg_input, mock_ffmpeg_probe + mock_bytesio, mock_image, mock_ffmpeg ): """Test that _update_thumbnails removes metadata for deleted files.""" # Setup basic mocks for Viewer init - mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + def listdir_side_effect(path): + if 'css' in path: + return ['bootstrap.min.css'] + elif 'js' in path: + return ['bootstrap.bundle.min.js'] + elif 'vid' in path or './vid' in path: + return ['test1.mp4'] # Only one file exists + return [] + + mock_listdir.side_effect = listdir_side_effect mock_abspath.side_effect = lambda x: x mock_basename.side_effect = lambda x: os.path.basename(x) mock_hash = MagicMock() mock_hash.digest.return_value = b'test_digest' mock_sha384.new.return_value = mock_hash + # Mock ffmpeg to avoid AttributeError + mock_ffmpeg.probe.return_value = { + 'streams': [{ + 'codec_type': 'video', + 'width': '1920', + 'height': '1080', + 'r_frame_rate': '30/1' + }] + } + mock_ffmpeg.input.return_value.filter.return_value.output.return_value.run.return_value = (b'\x00' * (1920 * 1080 * 3), b'') + + # Mock BytesIO for image saving + mock_io = MagicMock() + mock_io.getbuffer.return_value = b'image_data' + mock_bytesio.return_value.__enter__.return_value = mock_io + + # Mock PIL Image + mock_img = MagicMock() + mock_img.resize.return_value = mock_img + mock_image.frombytes.return_value = mock_img + + # Mock for Viewer.__init__ database connection mock_con_init = MagicMock() mock_cur_init = MagicMock() mock_cur_init.execute.return_value.fetchall.return_value = [] @@ -115,14 +141,11 @@ def test_update_thumbnails_removes_deleted_files( viewer = Viewer() viewer.hashes = {'css': {}, 'js': {}} - # Setup mocks - mock_basename.side_effect = lambda x: os.path.basename(x) - mock_abspath.side_effect = lambda x: x - mock_listdir.return_value = ['test1.mp4'] # Only one file exists - + # Mock for _update_thumbnails database connection mock_con = MagicMock() mock_cur = MagicMock() - # Database has 'deleted.mp4' which doesn't exist in filesystem + # First call: SELECT file FROM thumbnails (returns deleted.mp4) + # Second call: DELETE FROM thumbnails mock_cur.execute.return_value.fetchall.side_effect = [ [('deleted.mp4',)], # Existing metadata [] # After deletion @@ -137,8 +160,7 @@ def test_update_thumbnails_removes_deleted_files( if 'DELETE' in str(call)] assert len(delete_calls) > 0 - @patch('main.ffmpeg.probe') - @patch('main.ffmpeg.input') + @patch('main.ffmpeg') @patch('main.Image') @patch('main.BytesIO') @patch('main.connect') @@ -149,33 +171,35 @@ def test_update_thumbnails_removes_deleted_files( @patch('main.SHA384') def test_update_thumbnails_creates_metadata_for_new_files( self, mock_sha384, mock_file, mock_basename, mock_abspath, mock_listdir, mock_connect, - mock_bytesio, mock_image, mock_ffmpeg_input, mock_ffmpeg_probe + mock_bytesio, mock_image, mock_ffmpeg ): """Test that _update_thumbnails creates metadata for new MP4 files.""" # Setup basic mocks for Viewer init - mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + def listdir_side_effect(path): + if 'css' in path: + return ['bootstrap.min.css'] + elif 'js' in path: + return ['bootstrap.bundle.min.js'] + elif 'vid' in path or './vid' in path: + return ['new_video.mp4'] + return [] + + mock_listdir.side_effect = listdir_side_effect mock_abspath.side_effect = lambda x: x mock_basename.side_effect = lambda x: os.path.basename(x) mock_hash = MagicMock() mock_hash.digest.return_value = b'test_digest' mock_sha384.new.return_value = mock_hash + # Mock for Viewer.__init__ database connection mock_con_init = MagicMock() mock_cur_init = MagicMock() mock_cur_init.execute.return_value.fetchall.return_value = [] mock_con_init.cursor.return_value = mock_cur_init mock_connect.return_value.__enter__.return_value = mock_con_init - viewer = Viewer() - viewer.hashes = {'css': {}, 'js': {}} - - # Setup mocks - mock_basename.side_effect = lambda x: os.path.basename(x) - mock_abspath.side_effect = lambda x: x - mock_listdir.return_value = ['new_video.mp4'] - - # Mock ffmpeg probe - mock_ffmpeg_probe.return_value = { + # Mock ffmpeg probe (needed during Viewer.__init__) + mock_ffmpeg.probe.return_value = { 'streams': [{ 'codec_type': 'video', 'width': '1920', @@ -187,7 +211,7 @@ def test_update_thumbnails_creates_metadata_for_new_files( # Mock ffmpeg input/output mock_output = MagicMock() mock_output.run.return_value = (b'\x00' * (1920 * 1080 * 3), b'') - mock_ffmpeg_input.return_value.filter.return_value.output.return_value = mock_output + mock_ffmpeg.input.return_value.filter.return_value.output.return_value = mock_output # Mock PIL Image mock_img = MagicMock() @@ -199,9 +223,13 @@ def test_update_thumbnails_creates_metadata_for_new_files( mock_io.getbuffer.return_value = b'image_data' mock_bytesio.return_value.__enter__.return_value = mock_io + viewer = Viewer() + viewer.hashes = {'css': {}, 'js': {}} + + # Mock for _update_thumbnails database connection mock_con = MagicMock() mock_cur = MagicMock() - # No existing metadata + # First call: SELECT file FROM thumbnails (returns empty - no existing metadata) mock_cur.execute.return_value.fetchall.return_value = [] mock_con.cursor.return_value = mock_cur mock_connect.return_value.__enter__.return_value = mock_con @@ -224,7 +252,16 @@ def test_update_thumbnails_ignores_non_mp4_files( ): """Test that _update_thumbnails ignores non-MP4 files.""" # Setup basic mocks for Viewer init - mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + def listdir_side_effect(path): + if 'css' in path: + return ['bootstrap.min.css'] + elif 'js' in path: + return ['bootstrap.bundle.min.js'] + elif 'vid' in path or './vid' in path: + return ['video.txt', 'video.avi'] + return [] + + mock_listdir.side_effect = listdir_side_effect mock_abspath.side_effect = lambda x: x mock_basename.side_effect = lambda x: os.path.basename(x) mock_hash = MagicMock() @@ -240,10 +277,7 @@ def test_update_thumbnails_ignores_non_mp4_files( viewer = Viewer() viewer.hashes = {'css': {}, 'js': {}} - mock_basename.side_effect = lambda x: os.path.basename(x) - mock_abspath.side_effect = lambda x: x - mock_listdir.return_value = ['video.txt', 'video.avi'] - + # Mock for _update_thumbnails database connection mock_con = MagicMock() mock_cur = MagicMock() mock_cur.execute.return_value.fetchall.return_value = [] @@ -319,7 +353,16 @@ def test_index_displays_videos( ): """Test that index() displays video thumbnails.""" # Setup basic mocks for Viewer init - mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + def listdir_side_effect(path): + if 'css' in path: + return ['bootstrap.min.css'] + elif 'js' in path: + return ['bootstrap.bundle.min.js'] + elif 'vid' in path or './vid' in path: + return ['test1.mp4', 'test2.mp4'] + return [] + + mock_listdir.side_effect = listdir_side_effect mock_abspath.side_effect = lambda x: x mock_basename.side_effect = lambda x: os.path.basename(x) mock_hash = MagicMock() @@ -340,10 +383,7 @@ def test_index_displays_videos( 'js': {'bootstrap.bundle.min.js': 'test_hash'} } - mock_basename.side_effect = lambda x: os.path.basename(x) - mock_abspath.side_effect = lambda x: x - mock_listdir.return_value = ['test1.mp4', 'test2.mp4'] - + # Mock for index() database connection mock_con = MagicMock() mock_cur = MagicMock() mock_cur.execute.return_value.fetchone.return_value = ('base64_image_data',) @@ -368,11 +408,18 @@ class TestRefresh: @patch('main.SHA384') def test_refresh_calls_update_thumbnails( self, mock_sha384, mock_file, mock_basename, mock_abspath, - mock_listdir, mock_connect, mock_update, mock_redirect + mock_listdir, mock_connect, mock_redirect ): """Test that refresh() calls _update_thumbnails.""" # Setup basic mocks for Viewer init - mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + def listdir_side_effect(path): + if 'css' in path: + return ['bootstrap.min.css'] + elif 'js' in path: + return ['bootstrap.bundle.min.js'] + return [] + + mock_listdir.side_effect = listdir_side_effect mock_abspath.side_effect = lambda x: x mock_basename.side_effect = lambda x: os.path.basename(x) mock_hash = MagicMock() @@ -385,20 +432,43 @@ def test_refresh_calls_update_thumbnails( mock_con_init.cursor.return_value = mock_cur_init mock_connect.return_value.__enter__.return_value = mock_con_init - with patch.object(Viewer, '_update_thumbnails') as mock_update: - viewer = Viewer() - - viewer.hashes = {'css': {}, 'js': {}} - - mock_redirect_instance = MagicMock() - mock_redirect.return_value = mock_redirect_instance - - try: - viewer.refresh() - except: - pass # HTTPRedirect raises an exception - - mock_update.assert_called_once() + # Mock ffmpeg to avoid AttributeError during init + mock_ffmpeg_module = MagicMock() + mock_ffmpeg_module.probe.return_value = { + 'streams': [{ + 'codec_type': 'video', + 'width': '1920', + 'height': '1080', + 'r_frame_rate': '30/1' + }] + } + mock_ffmpeg_module.input.return_value.filter.return_value.output.return_value.run.return_value = (b'\x00' * (1920 * 1080 * 3), b'') + + with patch('main.ffmpeg', mock_ffmpeg_module), \ + patch('main.Image') as mock_image, \ + patch('main.BytesIO') as mock_bytesio: + # Mock BytesIO and Image + mock_io = MagicMock() + mock_io.getbuffer.return_value = b'image_data' + mock_bytesio.return_value.__enter__.return_value = mock_io + mock_img = MagicMock() + mock_img.resize.return_value = mock_img + mock_image.frombytes.return_value = mock_img + + with patch.object(Viewer, '_update_thumbnails') as mock_update: + viewer = Viewer() + viewer.hashes = {'css': {}, 'js': {}} + + mock_redirect_instance = MagicMock() + mock_redirect.return_value = mock_redirect_instance + + try: + viewer.refresh() + except: + pass # HTTPRedirect raises an exception + + # _update_thumbnails is called once during __init__ and once in refresh() + assert mock_update.call_count == 2 class TestVvid: @@ -415,7 +485,16 @@ def test_vvid_returns_html_for_valid_video( ): """Test that vvid() returns HTML for a valid video.""" # Setup basic mocks for Viewer init - mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + def listdir_side_effect(path): + if 'css' in path: + return ['bootstrap.min.css'] + elif 'js' in path: + return ['bootstrap.bundle.min.js'] + elif 'vid' in path or './vid' in path: + return ['test_video.mp4'] + return [] + + mock_listdir.side_effect = listdir_side_effect mock_abspath.side_effect = lambda x: x mock_basename.side_effect = lambda x: os.path.basename(x) mock_hash = MagicMock() @@ -443,10 +522,6 @@ def test_vvid_returns_html_for_valid_video( } } - mock_basename.side_effect = lambda x: os.path.basename(x) - mock_abspath.side_effect = lambda x: x - mock_listdir.return_value = ['test_video.mp4'] - result = viewer.vvid('test_video.mp4') assert isinstance(result, str) @@ -465,7 +540,16 @@ def test_vvid_returns_not_found_for_invalid_video( ): """Test that vvid() returns 'Not found' for invalid video.""" # Setup basic mocks for Viewer init - mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + def listdir_side_effect(path): + if 'css' in path: + return ['bootstrap.min.css'] + elif 'js' in path: + return ['bootstrap.bundle.min.js'] + elif 'vid' in path or './vid' in path: + return [] + return [] + + mock_listdir.side_effect = listdir_side_effect mock_abspath.side_effect = lambda x: x mock_basename.side_effect = lambda x: os.path.basename(x) mock_hash = MagicMock() @@ -482,14 +566,13 @@ def test_vvid_returns_not_found_for_invalid_video( viewer = Viewer() viewer.hashes = { - 'css': {'bootstrap.min.css': 'test_hash'}, + 'css': { + 'bootstrap.min.css': 'test_hash', + 'video-js.css': 'test_hash' + }, 'js': {'bootstrap.bundle.min.js': 'test_hash'} } - mock_basename.side_effect = lambda x: os.path.basename(x) - mock_abspath.side_effect = lambda x: x - mock_listdir.return_value = [] - result = viewer.vvid('nonexistent.mp4') assert 'Not found' in result @@ -505,7 +588,16 @@ def test_vvid_handles_default_parameter( ): """Test that vvid() handles default 'None' parameter.""" # Setup basic mocks for Viewer init - mock_listdir.side_effect = lambda x: ['bootstrap.min.css'] if 'css' in x else ['bootstrap.bundle.min.js'] + def listdir_side_effect(path): + if 'css' in path: + return ['bootstrap.min.css'] + elif 'js' in path: + return ['bootstrap.bundle.min.js'] + elif 'vid' in path or './vid' in path: + return [] + return [] + + mock_listdir.side_effect = listdir_side_effect mock_abspath.side_effect = lambda x: x mock_basename.side_effect = lambda x: os.path.basename(x) mock_hash = MagicMock() @@ -522,14 +614,13 @@ def test_vvid_handles_default_parameter( viewer = Viewer() viewer.hashes = { - 'css': {'bootstrap.min.css': 'test_hash'}, + 'css': { + 'bootstrap.min.css': 'test_hash', + 'video-js.css': 'test_hash' + }, 'js': {'bootstrap.bundle.min.js': 'test_hash'} } - mock_basename.side_effect = lambda x: os.path.basename(x) - mock_abspath.side_effect = lambda x: x - mock_listdir.return_value = [] - result = viewer.vvid() assert 'Not found' in result From 2a6a9d9bc428812983bdbe1ddce0e6ca07b0e734 Mon Sep 17 00:00:00 2001 From: Luis Eduardo Salazar Date: Sun, 30 Nov 2025 19:39:37 -0500 Subject: [PATCH 6/6] Remove CodeQL analysis step from CI workflow to streamline the continuous integration process. --- .github/workflows/ci.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3014b44..313dc9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,31 +57,3 @@ jobs: flake8 main.py __main__.py tests/ --count --select=E9,F63,F7,F82 --show-source --statistics flake8 main.py __main__.py tests/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - codeql-analysis: - name: CodeQL Analysis - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 -