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
64 changes: 63 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,66 @@ __pycache__/
/XXMI Launcher Config.json
/XXMI-Launcher Log.txt
/XXMI Launcher Log.txt
/fps_config.json
/fps_config.json

# Testing
.pytest_cache/
.coverage
htmlcov/
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytest_cache/
.tox/
.nox/

# Poetry
dist/
*.egg-info/

# Virtual environments
venv/
.venv/
env/
ENV/
.env

# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.project
.pydevproject

# Claude settings
.claude/*

# Build artifacts
build/
develop-eggs/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg

# Byte-compiled / optimized / DLL files
*.py[cod]
*$py.class
*.so

# Unit test / coverage reports
nosetests.xml
coverage.xml
*.cover
.coverage.*
.cache
1,170 changes: 1,170 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
[tool.poetry]
name = "xxmi-launcher"
version = "0.1.0"
description = "XXMI Launcher - A launcher tool for various game model importers"
authors = ["XXMI Team"]
readme = "README.md"
packages = [{include = "xxmi_launcher", from = "src"}]

[tool.poetry.dependencies]
python = ">3.9.1,<4.0"
certifi = "^2025.4.26"
cffi = "^1.17.1"
charset-normalizer = "^3.4.2"
cryptography = "^44.0.2"
customtkinter = "^5.2.2"
dacite = "^1.9.2"
darkdetect = "^0.8.0"
idna = "^3.10"
Markdown = "^3.8"
Nuitka = "^2.7"
ordered-set = "^4.1.0"
packaging = "^25.0"
pillow = "^11.2.1"
psutil = "^7.0.0"
py-gfm = "^2.0.0"
pycparser = "^2.22"
pyglet = "^2.1.6"
pyinjector = "^1.3.0"
PySocks = "^1.7.1"
pywin32 = {version = "^310", markers = "sys_platform == 'win32'"}
requests = "^2.32.3"
tkinterweb = "^4.3.1"
tkinterweb-tkhtml = "^1.0"
urllib3 = "^2.4.0"
winshell = {version = "^0.6", markers = "sys_platform == 'win32'"}
WMI = {version = "^1.5.1", markers = "sys_platform == 'win32'"}
zstandard = "^0.23.0"

[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
pytest-cov = "^5.0.0"
pytest-mock = "^3.14.0"

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

[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*", "*Tests"]
python_functions = ["test_*"]
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
"--cov=src/xxmi_launcher",
"--cov-branch",
"--cov-report=term-missing:skip-covered",
"--cov-report=html:htmlcov",
"--cov-report=xml:coverage.xml",
"--cov-fail-under=0", # TODO: Increase to 80 once tests are written
]
console_output_style = "progress"
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow running tests",
]

[tool.coverage.run]
source = ["src/xxmi_launcher"]
branch = true
parallel = true
omit = [
"*/tests/*",
"*/__pycache__/*",
"*/venv/*",
"*/.venv/*",
]

[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 # TODO: Increase to 80 once tests are written

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

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

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Empty file added tests/__init__.py
Empty file.
165 changes: 165 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""Shared pytest fixtures and configuration."""

import os
import sys
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch

import pytest

# Add src directory to Python path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))


@pytest.fixture
def temp_dir():
"""Create a temporary directory for test files."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)


@pytest.fixture
def mock_config_dir(temp_dir):
"""Create a mock configuration directory."""
config_dir = temp_dir / "config"
config_dir.mkdir(exist_ok=True)
return config_dir


@pytest.fixture
def mock_app_config():
"""Create a mock application configuration."""
return {
"general": {
"theme": "dark",
"language": "en",
"auto_update": True,
"show_notifications": True,
},
"paths": {
"game_path": "C:\\Games\\TestGame",
"mods_path": "C:\\Games\\TestGame\\Mods",
"backup_path": "C:\\Games\\TestGame\\Backups",
},
"launcher": {
"minimize_to_tray": False,
"start_with_windows": False,
"check_updates_on_start": True,
},
}


@pytest.fixture
def mock_game_info():
"""Create mock game information."""
return {
"name": "Test Game",
"version": "1.0.0",
"executable": "game.exe",
"installed": True,
"path": "C:\\Games\\TestGame",
}


@pytest.fixture
def mock_mod_info():
"""Create mock mod information."""
return {
"id": "test-mod-001",
"name": "Test Mod",
"version": "1.2.3",
"author": "Test Author",
"description": "A test mod for testing",
"enabled": True,
"files": ["mod.dll", "config.ini"],
}


@pytest.fixture
def mock_requests_get():
"""Mock requests.get for network tests."""
with patch('requests.get') as mock_get:
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"status": "success"}
mock_response.text = '{"status": "success"}'
mock_response.content = b'{"status": "success"}'
mock_get.return_value = mock_response
yield mock_get


@pytest.fixture
def mock_subprocess():
"""Mock subprocess calls."""
with patch('subprocess.run') as mock_run:
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = "Success"
mock_run.return_value.stderr = ""
yield mock_run


@pytest.fixture
def mock_logger():
"""Create a mock logger."""
logger = Mock()
logger.debug = Mock()
logger.info = Mock()
logger.warning = Mock()
logger.error = Mock()
logger.critical = Mock()
return logger


@pytest.fixture(autouse=True)
def reset_singletons():
"""Reset any singleton instances between tests."""
# This fixture runs automatically before each test
# Add any singleton reset logic here as needed
yield


@pytest.fixture
def mock_window():
"""Create a mock GUI window for testing GUI components."""
window = Mock()
window.winfo_x.return_value = 100
window.winfo_y.return_value = 100
window.winfo_width.return_value = 800
window.winfo_height.return_value = 600
window.title = Mock()
window.geometry = Mock()
window.mainloop = Mock()
window.destroy = Mock()
return window


@pytest.fixture
def clean_environment():
"""Ensure a clean test environment."""
# Store original environment
original_env = os.environ.copy()

# Clear specific env vars that might affect tests
test_env_vars = [
'XXMI_CONFIG_PATH',
'XXMI_LOG_LEVEL',
'XXMI_DEBUG',
]

for var in test_env_vars:
os.environ.pop(var, None)

yield

# Restore original environment
os.environ.clear()
os.environ.update(original_env)


@pytest.fixture
def mock_platform_system():
"""Mock platform.system for cross-platform tests."""
with patch('platform.system') as mock_system:
mock_system.return_value = 'Windows'
yield mock_system
Empty file added tests/integration/__init__.py
Empty file.
Loading