Skip to content
Open
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
38 changes: 37 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,37 @@
.idea/
.idea/

# Testing
.pytest_cache/
.coverage
htmlcov/
coverage.xml
*.pyc
__pycache__/

# Claude settings
.claude/*

# Build artifacts
build/
dist/
*.egg-info/
*.egg

# Virtual environments
venv/
env/
.env
.venv

# IDE files
.vscode/
*.swp
*.swo
*~
.DS_Store

# Logs
*.log

# Poetry lock file should NOT be ignored
# poetry.lock
719 changes: 719 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
[tool.poetry]
name = "videocrawlerengine"
version = "0.1.0"
description = "A video crawler engine for various video platforms"
authors = ["VideoCrawlerEngine Contributors"]
readme = "README.md"
license = "MIT"
packages = [{include = "core"}, {include = "gui"}, {include = "handler"}, {include = "nbdler"}]

[tool.poetry.dependencies]
python = ">=3.8,<3.13"
altgraph = "^0.17"
beautifulsoup4 = "^4.12"
bs4 = "^0.0.1"
future = "^0.18"
macholib = "^1.16"
pefile = "^2023.2"
Pillow = "^10.0"
PyInstaller = "^6.0"
pyperclip = "^1.8"
pyrasite = "^2.0"
pywin32-ctypes = {version = "^0.2.0", markers = "sys_platform == 'win32'"}
six = "^1.16"
soupsieve = "^2.5"
wxPython = {version = "^4.2", markers = "sys_platform != 'linux'", optional = true}

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
pytest-mock = "^3.11.0"

[tool.poetry.scripts]
test = "pytest:main"
tests = "pytest:main"

[tool.pytest.ini_options]
minversion = "7.0"
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
"--cov=core",
"--cov=nbdler",
"--cov-branch",
"--cov-report=term-missing:skip-covered",
"--cov-report=html:htmlcov",
"--cov-report=xml:coverage.xml",
"--cov-fail-under=0",
]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow running tests",
]

[tool.coverage.run]
source = ["core", "nbdler"]
omit = [
"*/tests/*",
"*/__init__.py",
"*/test_*.py",
"*_test.py",
"*/conftest.py",
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"def __str__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
precision = 2
show_missing = true
skip_covered = true
fail_under = 0

[tool.coverage.html]
directory = "htmlcov"

[tool.coverage.xml]
output = "coverage.xml"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Empty file added tests/__init__.py
Empty file.
233 changes: 233 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
"""Shared pytest fixtures and configuration for all tests."""
import json
import os
import shutil
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch

import pytest


@pytest.fixture
def temp_dir():
"""Create a temporary directory for test files."""
temp_path = tempfile.mkdtemp()
yield Path(temp_path)
shutil.rmtree(temp_path)


@pytest.fixture
def mock_config():
"""Provide a mock configuration object."""
return {
"download_path": "/tmp/downloads",
"cookies_path": "/tmp/cookies",
"max_threads": 4,
"timeout": 30,
"retry_attempts": 3,
"chunk_size": 1024 * 1024,
"user_agent": "TestAgent/1.0",
}


@pytest.fixture
def mock_video_info():
"""Provide mock video information."""
return {
"title": "Test Video",
"url": "https://example.com/video.mp4",
"duration": 300,
"size": 104857600, # 100MB
"format": "mp4",
"resolution": "1080p",
"site": "example",
}


@pytest.fixture
def mock_download_info():
"""Provide mock download information."""
return {
"url": "https://example.com/video.mp4",
"file_path": "/tmp/downloads/test_video.mp4",
"size": 104857600,
"downloaded": 0,
"status": "pending",
"speed": 0,
"eta": 0,
"threads": 4,
}


@pytest.fixture
def mock_cookies():
"""Provide mock cookies for testing."""
return {
"bilibili": {
"SESSDATA": "test_session_data",
"bili_jct": "test_csrf_token",
},
"iqiyi": {
"P00001": "test_auth_token",
"P00003": "test_user_id",
},
"tencent": {
"vqq_access_token": "test_access_token",
"vqq_appid": "test_app_id",
},
}


@pytest.fixture
def mock_http_response():
"""Provide a mock HTTP response."""
mock_resp = Mock()
mock_resp.status_code = 200
mock_resp.headers = {"Content-Type": "application/json"}
mock_resp.text = '{"status": "ok", "data": []}'
mock_resp.json.return_value = {"status": "ok", "data": []}
mock_resp.content = b'{"status": "ok", "data": []}'
return mock_resp


@pytest.fixture
def mock_requests(mock_http_response):
"""Mock the requests library."""
with patch("requests.get") as mock_get, \
patch("requests.post") as mock_post, \
patch("requests.Session") as mock_session:

mock_get.return_value = mock_http_response
mock_post.return_value = mock_http_response

session_instance = Mock()
session_instance.get.return_value = mock_http_response
session_instance.post.return_value = mock_http_response
mock_session.return_value = session_instance

yield {
"get": mock_get,
"post": mock_post,
"session": mock_session,
"session_instance": session_instance,
}


@pytest.fixture
def sample_m3u8_content():
"""Provide sample M3U8 playlist content."""
return """#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
segment0.ts
#EXTINF:10.0,
segment1.ts
#EXTINF:10.0,
segment2.ts
#EXT-X-ENDLIST"""


@pytest.fixture
def sample_html_content():
"""Provide sample HTML content for parsing tests."""
return """<!DOCTYPE html>
<html>
<head>
<title>Test Video Page</title>
</head>
<body>
<div class="video-container">
<video src="https://example.com/video.mp4" data-vid="12345"></video>
<h1 class="video-title">Test Video Title</h1>
<div class="video-info">
<span class="duration">05:00</span>
<span class="views">1,000,000</span>
</div>
</div>
</body>
</html>"""


@pytest.fixture
def mock_file_system(temp_dir):
"""Mock file system operations."""
def create_file(path, content=""):
file_path = temp_dir / path
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content)
return file_path

def create_directory(path):
dir_path = temp_dir / path
dir_path.mkdir(parents=True, exist_ok=True)
return dir_path

return {
"root": temp_dir,
"create_file": create_file,
"create_directory": create_directory,
}


@pytest.fixture(autouse=True)
def reset_environment():
"""Reset environment variables before each test."""
original_env = os.environ.copy()
yield
os.environ.clear()
os.environ.update(original_env)


@pytest.fixture
def capture_logs():
"""Capture log messages during tests."""
import logging
from io import StringIO

log_capture = StringIO()
handler = logging.StreamHandler(log_capture)
handler.setLevel(logging.DEBUG)

logger = logging.getLogger()
original_level = logger.level
logger.setLevel(logging.DEBUG)
logger.addHandler(handler)

yield log_capture

logger.removeHandler(handler)
logger.setLevel(original_level)


@pytest.fixture
def mock_threading():
"""Mock threading operations for testing."""
with patch("threading.Thread") as mock_thread_class:
threads = []

def mock_thread_init(target=None, args=None, kwargs=None, **other_kwargs):
thread = Mock()
thread.target = target
thread.args = args or ()
thread.kwargs = kwargs or {}
thread.is_alive.return_value = False
thread.start.side_effect = lambda: target(*thread.args, **thread.kwargs)
threads.append(thread)
return thread

mock_thread_class.side_effect = mock_thread_init

yield {"Thread": mock_thread_class, "threads": threads}


def pytest_configure(config):
"""Configure pytest with custom settings."""
config.addinivalue_line(
"markers", "network: mark test as requiring network access"
)
config.addinivalue_line(
"markers", "gui: mark test as requiring GUI components"
)
Empty file added tests/integration/__init__.py
Empty file.
Loading