Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CI

on:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["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 system dependencies
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg

- 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

15 changes: 12 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -225,15 +235,13 @@ 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.')
exit(3)
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()
Expand All @@ -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()
Expand Down
34 changes: 34 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[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"
pythonpath = ["."]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
]

10 changes: 10 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[pytest]
testpaths = tests
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"')

3 changes: 3 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pytest>=7.0.0
pytest-cov>=4.0.0

6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Python >= 3.11 required
cherrypy>=18.0.0
ffmpeg-python>=0.2.0
Pillow>=10.0.0
yattag>=1.0.0
pycryptodomex>=3.0.0
105 changes: 105 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -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

2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Tests package for vview

89 changes: 89 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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

Loading