mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 11:40:13 +01:00
tests (alleged)
This commit is contained in:
25
.github/workflows/python-app.yml
vendored
25
.github/workflows/python-app.yml
vendored
@@ -84,6 +84,31 @@ jobs:
|
|||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
run: uv python install
|
run: uv python install
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync
|
||||||
|
|
||||||
|
- name: Copy libaudio to project root (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cp libs/audio/*.dll . 2>/dev/null || echo "libaudio not found"
|
||||||
|
|
||||||
|
- name: Copy libaudio to project root (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cp libs/audio/libaudio.dylib . 2>/dev/null || echo "libaudio not found"
|
||||||
|
|
||||||
|
- name: Copy libaudio to project root (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cp libs/audio/libaudio.so . 2>/dev/null || echo "libaudio not found"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: uv run pytest test/libs/ -v --tb=short
|
||||||
|
continue-on-error: false
|
||||||
|
|
||||||
- name: Build Executable
|
- name: Build Executable
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
146
.github/workflows/tests.yml
vendored
Normal file
146
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master, develop ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-22.04, windows-latest, macos-latest]
|
||||||
|
python-version: ['3.12']
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check-out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Install libaudio Dependencies (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
brew update
|
||||||
|
brew install portaudio libsndfile speexdsp ccache
|
||||||
|
|
||||||
|
- name: Install libaudio Dependencies (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
uses: msys2/setup-msys2@v2
|
||||||
|
with:
|
||||||
|
update: true
|
||||||
|
install: >-
|
||||||
|
base-devel
|
||||||
|
mingw-w64-x86_64-gcc
|
||||||
|
mingw-w64-x86_64-libsndfile
|
||||||
|
mingw-w64-x86_64-speexdsp
|
||||||
|
mingw-w64-x86_64-ccache
|
||||||
|
|
||||||
|
- name: Install libaudio Dependencies (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
pkg-config \
|
||||||
|
libsndfile1-dev \
|
||||||
|
libspeexdsp-dev \
|
||||||
|
portaudio19-dev \
|
||||||
|
libpulse-dev \
|
||||||
|
ccache
|
||||||
|
|
||||||
|
- name: Build libaudio (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: msys2 {0}
|
||||||
|
run: |
|
||||||
|
cd libs/audio
|
||||||
|
make clean
|
||||||
|
make all
|
||||||
|
make verify
|
||||||
|
|
||||||
|
- name: Build libaudio (Unix)
|
||||||
|
if: runner.os != 'Windows'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cd libs/audio
|
||||||
|
make clean
|
||||||
|
make all
|
||||||
|
make verify
|
||||||
|
|
||||||
|
- name: Copy libaudio to project root (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cp libs/audio/*.dll . 2>/dev/null || echo "libaudio not found"
|
||||||
|
|
||||||
|
- name: Copy libaudio to project root (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cp libs/audio/libaudio.dylib . 2>/dev/null || echo "libaudio not found"
|
||||||
|
|
||||||
|
- name: Copy libaudio to project root (Linux)
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cp libs/audio/libaudio.so . 2>/dev/null || echo "libaudio not found"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v4
|
||||||
|
|
||||||
|
- name: Setup Python ${{ matrix.python-version }}
|
||||||
|
run: uv python install ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: uv run pytest test/libs/ -v --tb=short --color=yes
|
||||||
|
continue-on-error: false
|
||||||
|
|
||||||
|
- name: Run tests with coverage
|
||||||
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
|
run: |
|
||||||
|
uv run pytest test/libs/ --cov=libs --cov-report=xml --cov-report=html --cov-report=term
|
||||||
|
|
||||||
|
- name: Upload coverage reports
|
||||||
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
file: ./coverage.xml
|
||||||
|
flags: unittests
|
||||||
|
name: codecov-umbrella
|
||||||
|
fail_ci_if_error: false
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Upload test artifacts
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-results-${{ matrix.os }}-py${{ matrix.python-version }}
|
||||||
|
path: |
|
||||||
|
*.log
|
||||||
|
temp/
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
test-summary:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: test
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Test Summary
|
||||||
|
run: |
|
||||||
|
echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "All platform tests completed!" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -119,7 +119,8 @@ except OSError as e:
|
|||||||
|
|
||||||
class AudioEngine:
|
class AudioEngine:
|
||||||
"""Initialize an audio engine for playing sounds and music."""
|
"""Initialize an audio engine for playing sounds and music."""
|
||||||
def __init__(self, device_type: int, sample_rate: float, buffer_size: int, volume_presets: VolumeConfig):
|
def __init__(self, device_type: int, sample_rate: float, buffer_size: int,
|
||||||
|
volume_presets: VolumeConfig, sounds_path: Path | None = None):
|
||||||
self.device_type = max(device_type, 0)
|
self.device_type = max(device_type, 0)
|
||||||
if sample_rate < 0:
|
if sample_rate < 0:
|
||||||
self.target_sample_rate = 44100
|
self.target_sample_rate = 44100
|
||||||
@@ -131,7 +132,10 @@ class AudioEngine:
|
|||||||
self.audio_device_ready = False
|
self.audio_device_ready = False
|
||||||
self.volume_presets = volume_presets
|
self.volume_presets = volume_presets
|
||||||
|
|
||||||
self.sounds_path = Path(f"Skins/{get_config()["paths"]["skin"]}/Sounds")
|
if sounds_path is None:
|
||||||
|
self.sounds_path = Path(f"Skins/{get_config()['paths']['skin']}/Sounds")
|
||||||
|
else:
|
||||||
|
self.sounds_path = sounds_path
|
||||||
|
|
||||||
def set_log_level(self, level: int):
|
def set_log_level(self, level: int):
|
||||||
lib.set_log_level(level) # type: ignore
|
lib.set_log_level(level) # type: ignore
|
||||||
|
|||||||
@@ -7,10 +7,48 @@ requires-python = ">=3.13"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"av>=16.0.1",
|
"av>=16.0.1",
|
||||||
"pypresence>=4.6.1",
|
"pypresence>=4.6.1",
|
||||||
|
"pytest>=9.0.2",
|
||||||
"raylib-sdl>=5.5.0.2",
|
"raylib-sdl>=5.5.0.2",
|
||||||
"tomlkit>=0.13.3",
|
"tomlkit>=0.13.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["test"]
|
||||||
|
python_files = "test_*.py"
|
||||||
|
python_classes = "Test*"
|
||||||
|
python_functions = "test_*"
|
||||||
|
addopts = [
|
||||||
|
"-v",
|
||||||
|
"--strict-markers",
|
||||||
|
"--tb=short",
|
||||||
|
"--color=yes",
|
||||||
|
]
|
||||||
|
markers = [
|
||||||
|
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
||||||
|
"integration: marks tests as integration tests",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["libs"]
|
||||||
|
omit = [
|
||||||
|
"*/test/*",
|
||||||
|
"*/__pycache__/*",
|
||||||
|
"*/venv/*",
|
||||||
|
"*/.venv/*",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
"def __repr__",
|
||||||
|
"raise AssertionError",
|
||||||
|
"raise NotImplementedError",
|
||||||
|
"if __name__ == .__main__.:",
|
||||||
|
"if TYPE_CHECKING:",
|
||||||
|
"class .*\\bProtocol\\):",
|
||||||
|
"@(abc\\.)?abstractmethod",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.vulture]
|
[tool.vulture]
|
||||||
exclude = ["*.git", ".github/", ".venv/", "cache/"]
|
exclude = ["*.git", ".github/", ".venv/", "cache/"]
|
||||||
paths = ["."]
|
paths = ["."]
|
||||||
@@ -18,4 +56,5 @@ paths = ["."]
|
|||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"nuitka>=2.8.4",
|
"nuitka>=2.8.4",
|
||||||
|
"pytest-cov>=6.0.0",
|
||||||
]
|
]
|
||||||
|
|||||||
695
test/libs/test_animation.py
Normal file
695
test/libs/test_animation.py
Normal file
@@ -0,0 +1,695 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from libs.animation import (
|
||||||
|
Animation,
|
||||||
|
BaseAnimation,
|
||||||
|
FadeAnimation,
|
||||||
|
MoveAnimation,
|
||||||
|
TextStretchAnimation,
|
||||||
|
TextureChangeAnimation,
|
||||||
|
TextureResizeAnimation,
|
||||||
|
parse_animations,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseAnimation(unittest.TestCase):
|
||||||
|
"""Test cases for the BaseAnimation class."""
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
@patch('libs.animation.global_data')
|
||||||
|
def setUp(self, mock_global_data, mock_get_ms):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
mock_global_data.input_locked = 0
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_initialization(self, mock_get_ms):
|
||||||
|
"""Test basic initialization of BaseAnimation."""
|
||||||
|
mock_get_ms.return_value = 100.0
|
||||||
|
|
||||||
|
anim = BaseAnimation(duration=1000.0, delay=100.0, loop=True, lock_input=True)
|
||||||
|
|
||||||
|
self.assertEqual(anim.duration, 1000.0)
|
||||||
|
self.assertEqual(anim.delay, 100.0)
|
||||||
|
self.assertEqual(anim.delay_saved, 100.0)
|
||||||
|
self.assertEqual(anim.start_ms, 100.0)
|
||||||
|
self.assertFalse(anim.is_finished)
|
||||||
|
self.assertEqual(anim.attribute, 0)
|
||||||
|
self.assertFalse(anim.is_started)
|
||||||
|
self.assertTrue(anim.loop)
|
||||||
|
self.assertTrue(anim.lock_input)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
@patch('libs.animation.global_data')
|
||||||
|
def test_start(self, mock_global_data, mock_get_ms):
|
||||||
|
"""Test starting an animation."""
|
||||||
|
mock_get_ms.return_value = 200.0
|
||||||
|
mock_global_data.input_locked = 0
|
||||||
|
|
||||||
|
anim = BaseAnimation(duration=1000.0, lock_input=True)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
self.assertTrue(anim.is_started)
|
||||||
|
self.assertFalse(anim.is_finished)
|
||||||
|
self.assertEqual(mock_global_data.input_locked, 1)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
@patch('libs.animation.global_data')
|
||||||
|
def test_restart(self, mock_global_data, mock_get_ms):
|
||||||
|
"""Test restarting an animation."""
|
||||||
|
mock_get_ms.side_effect = [0.0, 500.0, 1000.0]
|
||||||
|
mock_global_data.input_locked = 0
|
||||||
|
|
||||||
|
anim = BaseAnimation(duration=1000.0, delay=100.0, lock_input=True)
|
||||||
|
anim.is_finished = True
|
||||||
|
anim.delay = 0.0
|
||||||
|
|
||||||
|
anim.restart()
|
||||||
|
|
||||||
|
self.assertEqual(anim.start_ms, 500.0)
|
||||||
|
self.assertFalse(anim.is_finished)
|
||||||
|
self.assertEqual(anim.delay, 100.0)
|
||||||
|
self.assertEqual(mock_global_data.input_locked, 1)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
@patch('libs.animation.global_data')
|
||||||
|
def test_pause_unpause(self, mock_global_data, mock_get_ms):
|
||||||
|
"""Test pausing and unpausing."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
mock_global_data.input_locked = 1
|
||||||
|
|
||||||
|
anim = BaseAnimation(duration=1000.0, lock_input=True)
|
||||||
|
anim.is_started = True
|
||||||
|
|
||||||
|
anim.pause()
|
||||||
|
self.assertFalse(anim.is_started)
|
||||||
|
self.assertEqual(mock_global_data.input_locked, 0)
|
||||||
|
|
||||||
|
anim.unpause()
|
||||||
|
self.assertTrue(anim.is_started)
|
||||||
|
self.assertEqual(mock_global_data.input_locked, 1)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_loop_restarts(self, mock_get_ms):
|
||||||
|
"""Test that looped animations restart when finished."""
|
||||||
|
mock_get_ms.side_effect = [0.0, 100.0]
|
||||||
|
|
||||||
|
anim = BaseAnimation(duration=1000.0, loop=True)
|
||||||
|
anim.is_finished = True
|
||||||
|
|
||||||
|
with patch.object(anim, 'restart') as mock_restart:
|
||||||
|
anim.update(100.0)
|
||||||
|
mock_restart.assert_called_once()
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
@patch('libs.animation.global_data')
|
||||||
|
def test_input_lock_unlock(self, mock_global_data, mock_get_ms):
|
||||||
|
"""Test input locking mechanism."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
mock_global_data.input_locked = 1
|
||||||
|
|
||||||
|
anim = BaseAnimation(duration=1000.0, lock_input=True)
|
||||||
|
anim.is_finished = True
|
||||||
|
anim.unlocked = False
|
||||||
|
|
||||||
|
anim.update(100.0)
|
||||||
|
|
||||||
|
self.assertTrue(anim.unlocked)
|
||||||
|
self.assertEqual(mock_global_data.input_locked, 0)
|
||||||
|
|
||||||
|
def test_easing_functions(self):
|
||||||
|
"""Test easing functions produce expected values."""
|
||||||
|
anim = BaseAnimation(duration=1000.0)
|
||||||
|
|
||||||
|
# Test quadratic ease in
|
||||||
|
self.assertAlmostEqual(anim._ease_in(0.5, "quadratic"), 0.25)
|
||||||
|
self.assertAlmostEqual(anim._ease_in(1.0, "quadratic"), 1.0)
|
||||||
|
|
||||||
|
# Test cubic ease in
|
||||||
|
self.assertAlmostEqual(anim._ease_in(0.5, "cubic"), 0.125)
|
||||||
|
|
||||||
|
# Test exponential ease in
|
||||||
|
self.assertEqual(anim._ease_in(0.0, "exponential"), 0)
|
||||||
|
|
||||||
|
# Test quadratic ease out
|
||||||
|
self.assertAlmostEqual(anim._ease_out(0.5, "quadratic"), 0.75)
|
||||||
|
|
||||||
|
# Test cubic ease out
|
||||||
|
self.assertAlmostEqual(anim._ease_out(0.5, "cubic"), 0.875)
|
||||||
|
|
||||||
|
# Test exponential ease out
|
||||||
|
self.assertEqual(anim._ease_out(1.0, "exponential"), 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFadeAnimation(unittest.TestCase):
|
||||||
|
"""Test cases for the FadeAnimation class."""
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_initialization(self, mock_get_ms):
|
||||||
|
"""Test fade animation initialization."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = FadeAnimation(
|
||||||
|
duration=1000.0,
|
||||||
|
initial_opacity=1.0,
|
||||||
|
final_opacity=0.0,
|
||||||
|
delay=100.0,
|
||||||
|
ease_in="quadratic"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(anim.initial_opacity, 1.0)
|
||||||
|
self.assertEqual(anim.final_opacity, 0.0)
|
||||||
|
self.assertEqual(anim.attribute, 1.0)
|
||||||
|
self.assertEqual(anim.ease_in, "quadratic")
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_fade_during_delay(self, mock_get_ms):
|
||||||
|
"""Test that opacity stays at initial during delay."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = FadeAnimation(duration=1000.0, initial_opacity=1.0, final_opacity=0.0, delay=500.0)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(250.0) # Within delay period
|
||||||
|
|
||||||
|
self.assertEqual(anim.attribute, 1.0)
|
||||||
|
self.assertFalse(anim.is_finished)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_fade_progression(self, mock_get_ms):
|
||||||
|
"""Test fade progresses correctly."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = FadeAnimation(duration=1000.0, initial_opacity=1.0, final_opacity=0.0)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(500.0) # Halfway through
|
||||||
|
|
||||||
|
self.assertAlmostEqual(anim.attribute, 0.5, places=2)
|
||||||
|
self.assertFalse(anim.is_finished)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_fade_completion(self, mock_get_ms):
|
||||||
|
"""Test fade completes at final opacity."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = FadeAnimation(duration=1000.0, initial_opacity=1.0, final_opacity=0.0)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(1000.0) # End of animation
|
||||||
|
|
||||||
|
self.assertEqual(anim.attribute, 0.0)
|
||||||
|
self.assertTrue(anim.is_finished)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_fade_with_reverse_delay(self, mock_get_ms):
|
||||||
|
"""Test fade reverses after reverse_delay."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = FadeAnimation(
|
||||||
|
duration=1000.0,
|
||||||
|
initial_opacity=1.0,
|
||||||
|
final_opacity=0.0,
|
||||||
|
reverse_delay=200.0
|
||||||
|
)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(1000.0) # Complete first fade
|
||||||
|
|
||||||
|
self.assertEqual(anim.attribute, 0.0)
|
||||||
|
self.assertTrue(anim.is_reversing)
|
||||||
|
self.assertEqual(anim.initial_opacity, 0.0)
|
||||||
|
self.assertEqual(anim.final_opacity, 1.0)
|
||||||
|
self.assertFalse(anim.is_finished)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_fade_with_easing(self, mock_get_ms):
|
||||||
|
"""Test fade applies easing correctly."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = FadeAnimation(
|
||||||
|
duration=1000.0,
|
||||||
|
initial_opacity=0.0,
|
||||||
|
final_opacity=1.0,
|
||||||
|
ease_in="quadratic"
|
||||||
|
)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(500.0) # Halfway
|
||||||
|
|
||||||
|
# With quadratic ease in, at 0.5 progress we should have 0.25
|
||||||
|
self.assertAlmostEqual(anim.attribute, 0.25, places=2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMoveAnimation(unittest.TestCase):
|
||||||
|
"""Test cases for the MoveAnimation class."""
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_initialization(self, mock_get_ms):
|
||||||
|
"""Test move animation initialization."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = MoveAnimation(
|
||||||
|
duration=1000.0,
|
||||||
|
start_position=0,
|
||||||
|
total_distance=100,
|
||||||
|
delay=50.0
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(anim.start_position, 0)
|
||||||
|
self.assertEqual(anim.total_distance, 100)
|
||||||
|
self.assertEqual(anim.delay, 50.0)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_move_during_delay(self, mock_get_ms):
|
||||||
|
"""Test position stays at start during delay."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = MoveAnimation(duration=1000.0, start_position=50, total_distance=100, delay=200.0)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(100.0) # Within delay
|
||||||
|
|
||||||
|
self.assertEqual(anim.attribute, 50)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_move_progression(self, mock_get_ms):
|
||||||
|
"""Test move progresses correctly."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = MoveAnimation(duration=1000.0, start_position=0, total_distance=100)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(500.0) # Halfway
|
||||||
|
|
||||||
|
self.assertAlmostEqual(anim.attribute, 50.0, places=2)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_move_completion(self, mock_get_ms):
|
||||||
|
"""Test move completes at final position."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = MoveAnimation(duration=1000.0, start_position=0, total_distance=100)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(1000.0)
|
||||||
|
|
||||||
|
self.assertEqual(anim.attribute, 100)
|
||||||
|
self.assertTrue(anim.is_finished)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_move_with_reverse_delay(self, mock_get_ms):
|
||||||
|
"""Test move reverses after reverse_delay."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = MoveAnimation(
|
||||||
|
duration=1000.0,
|
||||||
|
start_position=0,
|
||||||
|
total_distance=100,
|
||||||
|
reverse_delay=100.0
|
||||||
|
)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(1000.0) # Complete first move
|
||||||
|
|
||||||
|
self.assertEqual(anim.start_position, 100)
|
||||||
|
self.assertEqual(anim.total_distance, -100)
|
||||||
|
self.assertIsNone(anim.reverse_delay)
|
||||||
|
self.assertFalse(anim.is_finished)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_move_with_easing(self, mock_get_ms):
|
||||||
|
"""Test move applies easing."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = MoveAnimation(
|
||||||
|
duration=1000.0,
|
||||||
|
start_position=0,
|
||||||
|
total_distance=100,
|
||||||
|
ease_out="quadratic"
|
||||||
|
)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(500.0)
|
||||||
|
|
||||||
|
# With quadratic ease out, at 0.5 progress we should have 0.75
|
||||||
|
self.assertAlmostEqual(anim.attribute, 75.0, places=2)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTextureChangeAnimation(unittest.TestCase):
|
||||||
|
"""Test cases for the TextureChangeAnimation class."""
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_initialization(self, mock_get_ms):
|
||||||
|
"""Test texture change animation initialization."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
textures = [(0.0, 100.0, 0), (100.0, 200.0, 1), (200.0, 300.0, 2)]
|
||||||
|
anim = TextureChangeAnimation(duration=300.0, textures=textures)
|
||||||
|
|
||||||
|
self.assertEqual(anim.textures, textures)
|
||||||
|
self.assertEqual(anim.attribute, 0) # First texture index
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_texture_change_progression(self, mock_get_ms):
|
||||||
|
"""Test texture changes at correct times."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
textures = [(0.0, 100.0, 0), (100.0, 200.0, 1), (200.0, 300.0, 2)]
|
||||||
|
anim = TextureChangeAnimation(duration=300.0, textures=textures)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(50.0)
|
||||||
|
self.assertEqual(anim.attribute, 0)
|
||||||
|
|
||||||
|
anim.update(150.0)
|
||||||
|
self.assertEqual(anim.attribute, 1)
|
||||||
|
|
||||||
|
anim.update(250.0)
|
||||||
|
self.assertEqual(anim.attribute, 2)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_texture_change_completion(self, mock_get_ms):
|
||||||
|
"""Test texture change completes."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
textures = [(0.0, 100.0, 0), (100.0, 200.0, 1)]
|
||||||
|
anim = TextureChangeAnimation(duration=200.0, textures=textures)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(300.0) # Past duration
|
||||||
|
|
||||||
|
self.assertTrue(anim.is_finished)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_texture_change_with_delay(self, mock_get_ms):
|
||||||
|
"""Test texture change respects delay."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
textures = [(0.0, 100.0, 0), (100.0, 200.0, 1)]
|
||||||
|
anim = TextureChangeAnimation(duration=200.0, textures=textures, delay=100.0)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(50.0) # During delay
|
||||||
|
self.assertEqual(anim.attribute, 0)
|
||||||
|
|
||||||
|
anim.update(150.0) # 50ms into animation (after delay)
|
||||||
|
self.assertEqual(anim.attribute, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTextureResizeAnimation(unittest.TestCase):
|
||||||
|
"""Test cases for the TextureResizeAnimation class."""
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_initialization(self, mock_get_ms):
|
||||||
|
"""Test texture resize initialization."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = TextureResizeAnimation(
|
||||||
|
duration=1000.0,
|
||||||
|
initial_size=1.0,
|
||||||
|
final_size=2.0
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(anim.initial_size, 1.0)
|
||||||
|
self.assertEqual(anim.final_size, 2.0)
|
||||||
|
self.assertEqual(anim.attribute, 1.0)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_resize_progression(self, mock_get_ms):
|
||||||
|
"""Test resize progresses correctly."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = TextureResizeAnimation(duration=1000.0, initial_size=1.0, final_size=2.0)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(500.0) # Halfway
|
||||||
|
|
||||||
|
self.assertAlmostEqual(anim.attribute, 1.5, places=2)
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_resize_completion(self, mock_get_ms):
|
||||||
|
"""Test resize completes."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = TextureResizeAnimation(duration=1000.0, initial_size=1.0, final_size=0.5)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
anim.update(1000.0)
|
||||||
|
|
||||||
|
self.assertEqual(anim.attribute, 0.5)
|
||||||
|
self.assertTrue(anim.is_finished)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTextStretchAnimation(unittest.TestCase):
|
||||||
|
"""Test cases for the TextStretchAnimation class."""
|
||||||
|
|
||||||
|
@patch('libs.animation.get_current_ms')
|
||||||
|
def test_stretch_phases(self, mock_get_ms):
|
||||||
|
"""Test text stretch animation phases."""
|
||||||
|
mock_get_ms.return_value = 0.0
|
||||||
|
|
||||||
|
anim = TextStretchAnimation(duration=100.0)
|
||||||
|
anim.start()
|
||||||
|
|
||||||
|
# Phase 1: Growing
|
||||||
|
anim.update(50.0)
|
||||||
|
self.assertGreater(anim.attribute, 2)
|
||||||
|
|
||||||
|
# Phase 2: Shrinking back
|
||||||
|
anim.update(150.0)
|
||||||
|
self.assertGreater(anim.attribute, 0)
|
||||||
|
|
||||||
|
# Phase 3: Finished
|
||||||
|
anim.update(300.0)
|
||||||
|
self.assertEqual(anim.attribute, 0)
|
||||||
|
self.assertTrue(anim.is_finished)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAnimationFactory(unittest.TestCase):
|
||||||
|
"""Test cases for the Animation factory class."""
|
||||||
|
|
||||||
|
def test_create_fade(self):
|
||||||
|
"""Test factory creates fade animation."""
|
||||||
|
anim = Animation.create_fade(1000.0, initial_opacity=1.0, final_opacity=0.0)
|
||||||
|
|
||||||
|
self.assertIsInstance(anim, FadeAnimation)
|
||||||
|
self.assertEqual(anim.duration, 1000.0)
|
||||||
|
self.assertEqual(anim.initial_opacity, 1.0)
|
||||||
|
|
||||||
|
def test_create_move(self):
|
||||||
|
"""Test factory creates move animation."""
|
||||||
|
anim = Animation.create_move(1000.0, start_position=0, total_distance=100)
|
||||||
|
|
||||||
|
self.assertIsInstance(anim, MoveAnimation)
|
||||||
|
self.assertEqual(anim.duration, 1000.0)
|
||||||
|
self.assertEqual(anim.total_distance, 100)
|
||||||
|
|
||||||
|
def test_create_texture_change(self):
|
||||||
|
"""Test factory creates texture change animation."""
|
||||||
|
textures = [(0.0, 100.0, 0)]
|
||||||
|
anim = Animation.create_texture_change(1000.0, textures=textures)
|
||||||
|
|
||||||
|
self.assertIsInstance(anim, TextureChangeAnimation)
|
||||||
|
self.assertEqual(anim.textures, textures)
|
||||||
|
|
||||||
|
def test_create_texture_resize(self):
|
||||||
|
"""Test factory creates texture resize animation."""
|
||||||
|
anim = Animation.create_texture_resize(1000.0, initial_size=1.0, final_size=2.0)
|
||||||
|
|
||||||
|
self.assertIsInstance(anim, TextureResizeAnimation)
|
||||||
|
self.assertEqual(anim.initial_size, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseAnimations(unittest.TestCase):
|
||||||
|
"""Test cases for parse_animations function."""
|
||||||
|
|
||||||
|
def test_parse_basic_animation(self):
|
||||||
|
"""Test parsing a simple animation."""
|
||||||
|
animation_json = [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "fade",
|
||||||
|
"duration": 1000.0,
|
||||||
|
"initial_opacity": 1.0,
|
||||||
|
"final_opacity": 0.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
result = parse_animations(animation_json)
|
||||||
|
|
||||||
|
self.assertIn(1, result)
|
||||||
|
self.assertIsInstance(result[1], FadeAnimation)
|
||||||
|
self.assertEqual(result[1].duration, 1000.0)
|
||||||
|
|
||||||
|
def test_parse_multiple_animations(self):
|
||||||
|
"""Test parsing multiple animations."""
|
||||||
|
animation_json = [
|
||||||
|
{"id": 1, "type": "fade", "duration": 1000.0},
|
||||||
|
{"id": 2, "type": "move", "duration": 500.0, "total_distance": 50}
|
||||||
|
]
|
||||||
|
|
||||||
|
result = parse_animations(animation_json)
|
||||||
|
|
||||||
|
self.assertEqual(len(result), 2)
|
||||||
|
self.assertIsInstance(result[1], FadeAnimation)
|
||||||
|
self.assertIsInstance(result[2], MoveAnimation)
|
||||||
|
|
||||||
|
def test_parse_with_reference(self):
|
||||||
|
"""Test parsing animations with references."""
|
||||||
|
animation_json = [
|
||||||
|
{"id": 1, "type": "fade", "duration": 1000.0, "initial_opacity": 1.0},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "fade",
|
||||||
|
"duration": {"reference_id": 1, "property": "duration"},
|
||||||
|
"initial_opacity": 0.5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
result = parse_animations(animation_json)
|
||||||
|
|
||||||
|
self.assertEqual(result[2].duration, 1000.0)
|
||||||
|
self.assertEqual(result[2].initial_opacity, 0.5)
|
||||||
|
|
||||||
|
def test_parse_with_reference_and_init_val(self):
|
||||||
|
"""Test parsing with reference and init_val modifier."""
|
||||||
|
animation_json = [
|
||||||
|
{"id": 1, "type": "fade", "duration": 1000.0},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "fade",
|
||||||
|
"duration": {
|
||||||
|
"reference_id": 1,
|
||||||
|
"property": "duration",
|
||||||
|
"init_val": 500.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
result = parse_animations(animation_json)
|
||||||
|
|
||||||
|
self.assertEqual(result[2].duration, 1500.0)
|
||||||
|
|
||||||
|
def test_parse_missing_id_raises_error(self):
|
||||||
|
"""Test that missing id raises exception."""
|
||||||
|
animation_json = [
|
||||||
|
{"type": "fade", "duration": 1000.0}
|
||||||
|
]
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as context:
|
||||||
|
parse_animations(animation_json)
|
||||||
|
self.assertIn("requires id", str(context.exception))
|
||||||
|
|
||||||
|
def test_parse_missing_type_raises_error(self):
|
||||||
|
"""Test that missing type raises exception."""
|
||||||
|
animation_json = [
|
||||||
|
{"id": 1, "duration": 1000.0}
|
||||||
|
]
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as context:
|
||||||
|
parse_animations(animation_json)
|
||||||
|
self.assertIn("requires type", str(context.exception))
|
||||||
|
|
||||||
|
def test_parse_circular_reference_raises_error(self):
|
||||||
|
"""Test that circular references are detected."""
|
||||||
|
animation_json = [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "fade",
|
||||||
|
"duration": {"reference_id": 2, "property": "duration"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "fade",
|
||||||
|
"duration": {"reference_id": 1, "property": "duration"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as context:
|
||||||
|
parse_animations(animation_json)
|
||||||
|
self.assertIn("Circular reference", str(context.exception))
|
||||||
|
|
||||||
|
def test_parse_unknown_type_raises_error(self):
|
||||||
|
"""Test that unknown animation type raises exception."""
|
||||||
|
animation_json = [
|
||||||
|
{"id": 1, "type": "unknown_type", "duration": 1000.0}
|
||||||
|
]
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as context:
|
||||||
|
parse_animations(animation_json)
|
||||||
|
self.assertIn("Unknown Animation type", str(context.exception))
|
||||||
|
|
||||||
|
def test_parse_missing_reference_property_raises_error(self):
|
||||||
|
"""Test that missing reference property raises exception."""
|
||||||
|
animation_json = [
|
||||||
|
{"id": 1, "type": "fade", "duration": 1000.0},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "fade",
|
||||||
|
"duration": {"reference_id": 1}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as context:
|
||||||
|
parse_animations(animation_json)
|
||||||
|
self.assertIn("requires 'property'", str(context.exception))
|
||||||
|
|
||||||
|
def test_parse_nonexistent_reference_raises_error(self):
|
||||||
|
"""Test that referencing nonexistent animation raises exception."""
|
||||||
|
animation_json = [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "fade",
|
||||||
|
"duration": {"reference_id": 999, "property": "duration"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with self.assertRaises(Exception) as context:
|
||||||
|
parse_animations(animation_json)
|
||||||
|
self.assertIn("not found", str(context.exception))
|
||||||
|
|
||||||
|
def test_parse_ignores_comments(self):
|
||||||
|
"""Test that comments are ignored during parsing."""
|
||||||
|
animation_json = [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "fade",
|
||||||
|
"duration": 1000.0,
|
||||||
|
"comment": "This is a fade animation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
result = parse_animations(animation_json)
|
||||||
|
|
||||||
|
self.assertIn(1, result)
|
||||||
|
self.assertIsInstance(result[1], FadeAnimation)
|
||||||
|
|
||||||
|
def test_parse_nested_references(self):
|
||||||
|
"""Test parsing nested reference chains."""
|
||||||
|
animation_json = [
|
||||||
|
{"id": 1, "type": "fade", "duration": 1000.0},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "fade",
|
||||||
|
"duration": {"reference_id": 1, "property": "duration"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "fade",
|
||||||
|
"duration": {
|
||||||
|
"reference_id": 2,
|
||||||
|
"property": "duration",
|
||||||
|
"init_val": 500.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
result = parse_animations(animation_json)
|
||||||
|
|
||||||
|
self.assertEqual(result[3].duration, 1500.0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
518
test/libs/test_audio.py
Normal file
518
test/libs/test_audio.py
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
import shutil
|
||||||
|
import struct
|
||||||
|
import unittest
|
||||||
|
import wave
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from libs.audio import AudioEngine, audio
|
||||||
|
from libs.config import VolumeConfig
|
||||||
|
|
||||||
|
DEFAULT_CONFIG = VolumeConfig(sound=0.8, music=0.7, voice=0.6, hitsound=0.5, attract_mode=0.4)
|
||||||
|
|
||||||
|
class TestAudioEngine(unittest.TestCase):
|
||||||
|
"""Integration tests using the audio library."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
"""Set up test fixtures once for all tests."""
|
||||||
|
# Create temporary directory for test audio files
|
||||||
|
cls.test_dir = Path().cwd() / Path("temp")
|
||||||
|
cls.test_dir.mkdir(exist_ok=True)
|
||||||
|
cls.sounds_dir = Path(cls.test_dir) / "Sounds"
|
||||||
|
cls.sounds_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# Create test WAV files
|
||||||
|
cls._create_test_wav(cls.sounds_dir / "don.wav")
|
||||||
|
cls._create_test_wav(cls.sounds_dir / "ka.wav")
|
||||||
|
cls._create_test_wav(cls.sounds_dir / "test_sound.wav")
|
||||||
|
cls._create_test_wav(cls.sounds_dir / "test_music.wav", duration=2.0)
|
||||||
|
|
||||||
|
# Create screen sounds directory
|
||||||
|
cls.screen_sounds = cls.sounds_dir / "menu"
|
||||||
|
cls.screen_sounds.mkdir()
|
||||||
|
cls._create_test_wav(cls.screen_sounds / "click.wav")
|
||||||
|
cls._create_test_wav(cls.screen_sounds / "hover.wav")
|
||||||
|
|
||||||
|
# Create global sounds directory
|
||||||
|
cls.global_sounds = cls.sounds_dir / "global"
|
||||||
|
cls.global_sounds.mkdir()
|
||||||
|
cls._create_test_wav(cls.global_sounds / "confirm.wav")
|
||||||
|
|
||||||
|
cls.volume_presets = DEFAULT_CONFIG
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
"""Clean up test files."""
|
||||||
|
shutil.rmtree(cls.test_dir)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_test_wav(filepath, duration=0.1, frequency=440):
|
||||||
|
"""Create a simple test WAV file."""
|
||||||
|
sample_rate = 44100
|
||||||
|
num_samples = int(sample_rate * duration)
|
||||||
|
|
||||||
|
with wave.open(str(filepath), 'w') as wav_file:
|
||||||
|
wav_file.setnchannels(1) # Mono
|
||||||
|
wav_file.setsampwidth(2) # 16-bit
|
||||||
|
wav_file.setframerate(sample_rate)
|
||||||
|
|
||||||
|
for i in range(num_samples):
|
||||||
|
# Generate a simple sine wave
|
||||||
|
value = int(32767.0 * 0.3 *
|
||||||
|
(i % (sample_rate // frequency)) /
|
||||||
|
(sample_rate // frequency))
|
||||||
|
wav_file.writeframes(struct.pack('h', value))
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up each test."""
|
||||||
|
self.mock_config_path = self.sounds_dir
|
||||||
|
# Store original audio singleton state to avoid test pollution
|
||||||
|
self._original_audio_sounds_path = audio.sounds_path
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
"""Tear down each test."""
|
||||||
|
# Restore original audio singleton state
|
||||||
|
audio.sounds_path = self._original_audio_sounds_path
|
||||||
|
# Clear any sounds or music loaded during tests
|
||||||
|
if hasattr(audio, 'sounds') and isinstance(audio.sounds, dict):
|
||||||
|
audio.sounds.clear()
|
||||||
|
if hasattr(audio, 'music_streams') and isinstance(audio.music_streams, dict):
|
||||||
|
audio.music_streams.clear()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_initialization(self, mock_config):
|
||||||
|
"""Test AudioEngine initialization."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
|
||||||
|
engine = AudioEngine(
|
||||||
|
device_type=0,
|
||||||
|
sample_rate=44100.0,
|
||||||
|
buffer_size=512,
|
||||||
|
volume_presets=self.volume_presets,
|
||||||
|
sounds_path=self.sounds_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(engine.device_type, 0)
|
||||||
|
self.assertEqual(engine.target_sample_rate, 44100.0)
|
||||||
|
self.assertEqual(engine.buffer_size, 512)
|
||||||
|
self.assertEqual(engine.volume_presets, self.volume_presets)
|
||||||
|
self.assertFalse(engine.audio_device_ready)
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_init_and_close_audio_device(self, mock_config):
|
||||||
|
"""Test initializing and closing audio device."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
# Initialize
|
||||||
|
success = engine.init_audio_device()
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertTrue(engine.audio_device_ready)
|
||||||
|
self.assertTrue(engine.is_audio_device_ready())
|
||||||
|
|
||||||
|
# Close
|
||||||
|
engine.close_audio_device()
|
||||||
|
self.assertFalse(engine.audio_device_ready)
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_master_volume(self, mock_config):
|
||||||
|
"""Test master volume control."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
# Set and get master volume
|
||||||
|
engine.set_master_volume(0.75)
|
||||||
|
volume = engine.get_master_volume()
|
||||||
|
self.assertAlmostEqual(volume, 0.75, places=2)
|
||||||
|
|
||||||
|
# Test clamping
|
||||||
|
engine.set_master_volume(1.5)
|
||||||
|
volume = engine.get_master_volume()
|
||||||
|
self.assertLessEqual(volume, 1.0)
|
||||||
|
|
||||||
|
engine.set_master_volume(-0.5)
|
||||||
|
volume = engine.get_master_volume()
|
||||||
|
self.assertGreaterEqual(volume, 0.0)
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_load_and_unload_sound(self, mock_config):
|
||||||
|
"""Test loading and unloading sounds."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
# Load sound
|
||||||
|
sound_path = self.sounds_dir / "test_sound.wav"
|
||||||
|
sound_id = engine.load_sound(sound_path, "test")
|
||||||
|
|
||||||
|
self.assertEqual(sound_id, "test")
|
||||||
|
self.assertIn("test", engine.sounds)
|
||||||
|
|
||||||
|
# Unload sound
|
||||||
|
engine.unload_sound("test")
|
||||||
|
self.assertNotIn("test", engine.sounds)
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_load_nonexistent_sound(self, mock_config):
|
||||||
|
"""Test loading a non-existent sound file."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
sound_id = engine.load_sound(Path("nonexistent.wav"), "bad")
|
||||||
|
self.assertEqual(sound_id, "")
|
||||||
|
self.assertNotIn("bad", engine.sounds)
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_play_and_stop_sound(self, mock_config):
|
||||||
|
"""Test playing and stopping sounds."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
# Load and play sound
|
||||||
|
sound_path = self.sounds_dir / "test_sound.wav"
|
||||||
|
engine.load_sound(sound_path, "test")
|
||||||
|
|
||||||
|
engine.play_sound("test", "sound")
|
||||||
|
|
||||||
|
# Give it a moment to start
|
||||||
|
import time
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
# Check if playing (might not be if audio is very short)
|
||||||
|
# Just verify no exceptions were raised
|
||||||
|
is_playing = engine.is_sound_playing("test")
|
||||||
|
self.assertIsInstance(is_playing, bool)
|
||||||
|
|
||||||
|
# Stop sound
|
||||||
|
engine.stop_sound("test")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_play_don_and_kat(self, mock_config):
|
||||||
|
"""Test playing the special don and kat sounds."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
# Play don
|
||||||
|
engine.play_sound("don", "sound")
|
||||||
|
is_playing = engine.is_sound_playing("don")
|
||||||
|
self.assertIsInstance(is_playing, bool)
|
||||||
|
engine.stop_sound("don")
|
||||||
|
|
||||||
|
# Play kat
|
||||||
|
engine.play_sound("kat", "sound")
|
||||||
|
is_playing = engine.is_sound_playing("kat")
|
||||||
|
self.assertIsInstance(is_playing, bool)
|
||||||
|
engine.stop_sound("kat")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_sound_volume_control(self, mock_config):
|
||||||
|
"""Test setting sound volume."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
sound_path = self.sounds_dir / "test_sound.wav"
|
||||||
|
engine.load_sound(sound_path, "test")
|
||||||
|
|
||||||
|
# Set volume (should not raise exception)
|
||||||
|
engine.set_sound_volume("test", 0.5)
|
||||||
|
engine.play_sound("test", "")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_sound_pan_control(self, mock_config):
|
||||||
|
"""Test setting sound pan."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
sound_path = self.sounds_dir / "test_sound.wav"
|
||||||
|
engine.load_sound(sound_path, "test")
|
||||||
|
|
||||||
|
# Set pan (should not raise exception)
|
||||||
|
engine.set_sound_pan("test", -0.5) # Left
|
||||||
|
engine.set_sound_pan("test", 0.5) # Right
|
||||||
|
engine.set_sound_pan("test", 0.0) # Center
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_load_screen_sounds(self, mock_config):
|
||||||
|
"""Test loading sounds for a screen."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
engine.load_screen_sounds("menu")
|
||||||
|
|
||||||
|
# Check that screen sounds were loaded
|
||||||
|
self.assertIn("click", engine.sounds)
|
||||||
|
self.assertIn("hover", engine.sounds)
|
||||||
|
|
||||||
|
# Check that global sounds were loaded
|
||||||
|
self.assertIn("confirm", engine.sounds)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_unload_all_sounds(self, mock_config):
|
||||||
|
"""Test unloading all sounds."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
# Load multiple sounds
|
||||||
|
engine.load_sound(self.sounds_dir / "test_sound.wav", "s1")
|
||||||
|
engine.load_sound(self.sounds_dir / "test_sound.wav", "s2")
|
||||||
|
engine.load_sound(self.sounds_dir / "test_sound.wav", "s3")
|
||||||
|
|
||||||
|
self.assertEqual(len(engine.sounds), 3)
|
||||||
|
|
||||||
|
# Unload all
|
||||||
|
engine.unload_all_sounds()
|
||||||
|
self.assertEqual(len(engine.sounds), 0)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_load_and_play_music_stream(self, mock_config):
|
||||||
|
"""Test loading and playing music streams."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
music_path = self.sounds_dir / "test_music.wav"
|
||||||
|
music_id = engine.load_music_stream(music_path, "bgm")
|
||||||
|
print(music_id)
|
||||||
|
|
||||||
|
self.assertEqual(music_id, "bgm")
|
||||||
|
self.assertIn("bgm", engine.music_streams)
|
||||||
|
|
||||||
|
# Play music
|
||||||
|
engine.play_music_stream("bgm", "music")
|
||||||
|
|
||||||
|
# Update music stream
|
||||||
|
engine.update_music_stream("bgm")
|
||||||
|
|
||||||
|
# Check if playing
|
||||||
|
is_playing = engine.is_music_stream_playing("bgm")
|
||||||
|
self.assertIsInstance(is_playing, bool)
|
||||||
|
|
||||||
|
# Stop music
|
||||||
|
engine.stop_music_stream("bgm")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_music_time_functions(self, mock_config):
|
||||||
|
"""Test getting music time length and played."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
music_path = self.sounds_dir / "test_music.wav"
|
||||||
|
music_file = engine.load_music_stream(music_path, "bgm")
|
||||||
|
|
||||||
|
# Get time length
|
||||||
|
length = engine.get_music_time_length(music_file)
|
||||||
|
self.assertGreater(length, 0.0)
|
||||||
|
self.assertLess(length, 10.0) # Should be around 2 seconds
|
||||||
|
|
||||||
|
# Play and get time played
|
||||||
|
engine.play_music_stream(music_file, "music")
|
||||||
|
engine.update_music_stream(music_file)
|
||||||
|
|
||||||
|
import time
|
||||||
|
time.sleep(0.1)
|
||||||
|
engine.update_music_stream(music_file)
|
||||||
|
|
||||||
|
played = engine.get_music_time_played(music_file)
|
||||||
|
self.assertGreaterEqual(played, 0.0)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_music_volume_control(self, mock_config):
|
||||||
|
"""Test setting music volume."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
music_path = self.sounds_dir / "test_music.wav"
|
||||||
|
engine.load_music_stream(music_path, "bgm")
|
||||||
|
|
||||||
|
# Set volume (should not raise exception)
|
||||||
|
engine.set_music_volume("bgm", 0.6)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_seek_music_stream(self, mock_config):
|
||||||
|
"""Test seeking in music stream."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
music_path = self.sounds_dir / "test_music.wav"
|
||||||
|
engine.load_music_stream(music_path, "bgm")
|
||||||
|
|
||||||
|
# Seek to position (should not raise exception)
|
||||||
|
engine.seek_music_stream("bgm", 0.5)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_unload_music_stream(self, mock_config):
|
||||||
|
"""Test unloading music stream."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
music_path = self.sounds_dir / "test_music.wav"
|
||||||
|
engine.load_music_stream(music_path, "bgm")
|
||||||
|
|
||||||
|
self.assertIn("bgm", engine.music_streams)
|
||||||
|
|
||||||
|
engine.unload_music_stream("bgm")
|
||||||
|
self.assertNotIn("bgm", engine.music_streams)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_unload_all_music(self, mock_config):
|
||||||
|
"""Test unloading all music streams."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
# Load multiple music streams
|
||||||
|
music_path = self.sounds_dir / "test_music.wav"
|
||||||
|
engine.load_music_stream(music_path, "bgm1")
|
||||||
|
engine.load_music_stream(music_path, "bgm2")
|
||||||
|
|
||||||
|
self.assertEqual(len(engine.music_streams), 2)
|
||||||
|
|
||||||
|
engine.unload_all_music()
|
||||||
|
self.assertEqual(len(engine.music_streams), 0)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_host_api_functions(self, mock_config):
|
||||||
|
"""Test host API query functions."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
engine.init_audio_device()
|
||||||
|
|
||||||
|
# List host APIs (should not crash)
|
||||||
|
engine.list_host_apis()
|
||||||
|
|
||||||
|
# Get host API name
|
||||||
|
name = engine.get_host_api_name(0)
|
||||||
|
self.assertIsInstance(name, str)
|
||||||
|
|
||||||
|
@patch('libs.audio.get_config')
|
||||||
|
def test_full_lifecycle(self, mock_config):
|
||||||
|
"""Test complete audio engine lifecycle."""
|
||||||
|
mock_config.return_value = {"paths": {"skin": f"{self.sounds_dir}"}}
|
||||||
|
engine = AudioEngine(0, 44100.0, 512, self.volume_presets, sounds_path=self.sounds_dir)
|
||||||
|
engine.set_log_level(10)
|
||||||
|
|
||||||
|
if engine.init_audio_device():
|
||||||
|
try:
|
||||||
|
# Load sounds and music
|
||||||
|
engine.load_sound(self.sounds_dir / "test_sound.wav", "sfx")
|
||||||
|
engine.load_music_stream(self.sounds_dir / "test_music.wav", "bgm")
|
||||||
|
|
||||||
|
# Set volumes
|
||||||
|
engine.set_master_volume(0.8)
|
||||||
|
engine.set_sound_volume("sfx", 0.7)
|
||||||
|
engine.set_music_volume("bgm", 0.6)
|
||||||
|
|
||||||
|
# Play audio
|
||||||
|
engine.play_sound("sfx", "sound")
|
||||||
|
engine.play_music_stream("bgm", "music")
|
||||||
|
|
||||||
|
import time
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Update music
|
||||||
|
engine.update_music_stream("bgm")
|
||||||
|
|
||||||
|
# Stop
|
||||||
|
engine.stop_sound("sfx")
|
||||||
|
engine.stop_music_stream("bgm")
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
engine.unload_sound("sfx")
|
||||||
|
engine.unload_music_stream("bgm")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
engine.close_audio_device()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Run tests
|
||||||
|
unittest.main(verbosity=2)
|
||||||
551
test/libs/test_global_data.py
Normal file
551
test/libs/test_global_data.py
Normal file
@@ -0,0 +1,551 @@
|
|||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from libs.global_data import (
|
||||||
|
Camera,
|
||||||
|
Crown,
|
||||||
|
DanResultData,
|
||||||
|
DanResultExam,
|
||||||
|
DanResultSong,
|
||||||
|
Difficulty,
|
||||||
|
GlobalData,
|
||||||
|
Modifiers,
|
||||||
|
PlayerNum,
|
||||||
|
ResultData,
|
||||||
|
ScoreMethod,
|
||||||
|
SessionData,
|
||||||
|
global_data,
|
||||||
|
reset_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlayerNum(unittest.TestCase):
|
||||||
|
"""Test cases for the PlayerNum enum."""
|
||||||
|
|
||||||
|
def test_player_num_values(self):
|
||||||
|
"""Test PlayerNum enum values."""
|
||||||
|
self.assertEqual(PlayerNum.ALL, 0)
|
||||||
|
self.assertEqual(PlayerNum.P1, 1)
|
||||||
|
self.assertEqual(PlayerNum.P2, 2)
|
||||||
|
self.assertEqual(PlayerNum.TWO_PLAYER, 3)
|
||||||
|
self.assertEqual(PlayerNum.DAN, 4)
|
||||||
|
|
||||||
|
def test_player_num_is_int_enum(self):
|
||||||
|
"""Test that PlayerNum values are integers."""
|
||||||
|
self.assertIsInstance(PlayerNum.P1, int)
|
||||||
|
self.assertIsInstance(PlayerNum.P2, int)
|
||||||
|
|
||||||
|
|
||||||
|
class TestScoreMethod(unittest.TestCase):
|
||||||
|
"""Test cases for the ScoreMethod class."""
|
||||||
|
|
||||||
|
def test_score_method_constants(self):
|
||||||
|
"""Test ScoreMethod constants."""
|
||||||
|
self.assertEqual(ScoreMethod.GEN3, "gen3")
|
||||||
|
self.assertEqual(ScoreMethod.SHINUCHI, "shinuchi")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDifficulty(unittest.TestCase):
|
||||||
|
"""Test cases for the Difficulty enum."""
|
||||||
|
|
||||||
|
def test_difficulty_values(self):
|
||||||
|
"""Test Difficulty enum values."""
|
||||||
|
self.assertEqual(Difficulty.EASY, 0)
|
||||||
|
self.assertEqual(Difficulty.NORMAL, 1)
|
||||||
|
self.assertEqual(Difficulty.HARD, 2)
|
||||||
|
self.assertEqual(Difficulty.ONI, 3)
|
||||||
|
self.assertEqual(Difficulty.URA, 4)
|
||||||
|
self.assertEqual(Difficulty.TOWER, 5)
|
||||||
|
self.assertEqual(Difficulty.DAN, 6)
|
||||||
|
|
||||||
|
def test_difficulty_ordering(self):
|
||||||
|
"""Test that difficulty levels are ordered correctly."""
|
||||||
|
self.assertLess(Difficulty.EASY, Difficulty.NORMAL)
|
||||||
|
self.assertLess(Difficulty.NORMAL, Difficulty.HARD)
|
||||||
|
self.assertLess(Difficulty.HARD, Difficulty.ONI)
|
||||||
|
self.assertLess(Difficulty.ONI, Difficulty.URA)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrown(unittest.TestCase):
|
||||||
|
"""Test cases for the Crown enum."""
|
||||||
|
|
||||||
|
def test_crown_values(self):
|
||||||
|
"""Test Crown enum values."""
|
||||||
|
self.assertEqual(Crown.NONE, 0)
|
||||||
|
self.assertEqual(Crown.CLEAR, 1)
|
||||||
|
self.assertEqual(Crown.FC, 2)
|
||||||
|
self.assertEqual(Crown.DFC, 3)
|
||||||
|
|
||||||
|
def test_crown_ordering(self):
|
||||||
|
"""Test crown achievement ordering."""
|
||||||
|
self.assertLess(Crown.NONE, Crown.CLEAR)
|
||||||
|
self.assertLess(Crown.CLEAR, Crown.FC)
|
||||||
|
self.assertLess(Crown.FC, Crown.DFC)
|
||||||
|
|
||||||
|
|
||||||
|
class TestModifiers(unittest.TestCase):
|
||||||
|
"""Test cases for the Modifiers dataclass."""
|
||||||
|
|
||||||
|
def test_default_values(self):
|
||||||
|
"""Test default modifier values."""
|
||||||
|
mods = Modifiers()
|
||||||
|
|
||||||
|
self.assertFalse(mods.auto)
|
||||||
|
self.assertEqual(mods.speed, 1.0)
|
||||||
|
self.assertFalse(mods.display)
|
||||||
|
self.assertFalse(mods.inverse)
|
||||||
|
self.assertEqual(mods.random, 0)
|
||||||
|
|
||||||
|
def test_custom_values(self):
|
||||||
|
"""Test custom modifier values."""
|
||||||
|
mods = Modifiers(auto=True, speed=2.0, display=True, inverse=True, random=3)
|
||||||
|
|
||||||
|
self.assertTrue(mods.auto)
|
||||||
|
self.assertEqual(mods.speed, 2.0)
|
||||||
|
self.assertTrue(mods.display)
|
||||||
|
self.assertTrue(mods.inverse)
|
||||||
|
self.assertEqual(mods.random, 3)
|
||||||
|
|
||||||
|
def test_speed_multiplier(self):
|
||||||
|
"""Test different speed multiplier values."""
|
||||||
|
mods1 = Modifiers(speed=0.5)
|
||||||
|
mods2 = Modifiers(speed=1.5)
|
||||||
|
mods3 = Modifiers(speed=3.0)
|
||||||
|
|
||||||
|
self.assertEqual(mods1.speed, 0.5)
|
||||||
|
self.assertEqual(mods2.speed, 1.5)
|
||||||
|
self.assertEqual(mods3.speed, 3.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDanResultSong(unittest.TestCase):
|
||||||
|
"""Test cases for the DanResultSong dataclass."""
|
||||||
|
|
||||||
|
def test_default_values(self):
|
||||||
|
"""Test default DanResultSong values."""
|
||||||
|
song = DanResultSong()
|
||||||
|
|
||||||
|
self.assertEqual(song.selected_difficulty, 0)
|
||||||
|
self.assertEqual(song.diff_level, 0)
|
||||||
|
self.assertEqual(song.song_title, "default_title")
|
||||||
|
self.assertEqual(song.genre_index, 0)
|
||||||
|
self.assertEqual(song.good, 0)
|
||||||
|
self.assertEqual(song.ok, 0)
|
||||||
|
self.assertEqual(song.bad, 0)
|
||||||
|
self.assertEqual(song.drumroll, 0)
|
||||||
|
|
||||||
|
def test_custom_values(self):
|
||||||
|
"""Test custom DanResultSong values."""
|
||||||
|
song = DanResultSong(
|
||||||
|
selected_difficulty=3,
|
||||||
|
diff_level=10,
|
||||||
|
song_title="Test Song",
|
||||||
|
genre_index=5,
|
||||||
|
good=100,
|
||||||
|
ok=20,
|
||||||
|
bad=5,
|
||||||
|
drumroll=15
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(song.selected_difficulty, 3)
|
||||||
|
self.assertEqual(song.diff_level, 10)
|
||||||
|
self.assertEqual(song.song_title, "Test Song")
|
||||||
|
self.assertEqual(song.genre_index, 5)
|
||||||
|
self.assertEqual(song.good, 100)
|
||||||
|
self.assertEqual(song.ok, 20)
|
||||||
|
self.assertEqual(song.bad, 5)
|
||||||
|
self.assertEqual(song.drumroll, 15)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDanResultExam(unittest.TestCase):
|
||||||
|
"""Test cases for the DanResultExam class."""
|
||||||
|
|
||||||
|
def test_default_values(self):
|
||||||
|
"""Test default DanResultExam values."""
|
||||||
|
exam = DanResultExam()
|
||||||
|
|
||||||
|
self.assertEqual(exam.progress, 0)
|
||||||
|
self.assertEqual(exam.counter_value, 0)
|
||||||
|
self.assertEqual(exam.bar_texture, "exam_red")
|
||||||
|
self.assertFalse(exam.failed)
|
||||||
|
|
||||||
|
def test_custom_values(self):
|
||||||
|
"""Test custom DanResultExam values."""
|
||||||
|
exam = DanResultExam()
|
||||||
|
exam.progress = 0.75
|
||||||
|
exam.counter_value = 150
|
||||||
|
exam.bar_texture = "exam_gold"
|
||||||
|
exam.failed = True
|
||||||
|
|
||||||
|
self.assertEqual(exam.progress, 0.75)
|
||||||
|
self.assertEqual(exam.counter_value, 150)
|
||||||
|
self.assertEqual(exam.bar_texture, "exam_gold")
|
||||||
|
self.assertTrue(exam.failed)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDanResultData(unittest.TestCase):
|
||||||
|
"""Test cases for the DanResultData dataclass."""
|
||||||
|
|
||||||
|
def test_default_values(self):
|
||||||
|
"""Test default DanResultData values."""
|
||||||
|
data = DanResultData()
|
||||||
|
|
||||||
|
self.assertEqual(data.dan_color, 0)
|
||||||
|
self.assertEqual(data.dan_title, "default_title")
|
||||||
|
self.assertEqual(data.score, 0)
|
||||||
|
self.assertEqual(data.gauge_length, 0.0)
|
||||||
|
self.assertEqual(data.max_combo, 0)
|
||||||
|
self.assertEqual(data.songs, [])
|
||||||
|
self.assertEqual(data.exams, [])
|
||||||
|
self.assertEqual(data.exam_data, [])
|
||||||
|
|
||||||
|
def test_with_songs(self):
|
||||||
|
"""Test DanResultData with songs."""
|
||||||
|
song1 = DanResultSong(song_title="Song 1")
|
||||||
|
song2 = DanResultSong(song_title="Song 2")
|
||||||
|
|
||||||
|
data = DanResultData(songs=[song1, song2])
|
||||||
|
|
||||||
|
self.assertEqual(len(data.songs), 2)
|
||||||
|
self.assertEqual(data.songs[0].song_title, "Song 1")
|
||||||
|
self.assertEqual(data.songs[1].song_title, "Song 2")
|
||||||
|
|
||||||
|
def test_with_exam_data(self):
|
||||||
|
"""Test DanResultData with exam data."""
|
||||||
|
exam1 = DanResultExam()
|
||||||
|
exam1.progress = 0.5
|
||||||
|
exam2 = DanResultExam()
|
||||||
|
exam2.progress = 1.0
|
||||||
|
|
||||||
|
data = DanResultData(exam_data=[exam1, exam2])
|
||||||
|
|
||||||
|
self.assertEqual(len(data.exam_data), 2)
|
||||||
|
self.assertEqual(data.exam_data[0].progress, 0.5)
|
||||||
|
self.assertEqual(data.exam_data[1].progress, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestResultData(unittest.TestCase):
|
||||||
|
"""Test cases for the ResultData dataclass."""
|
||||||
|
|
||||||
|
def test_default_values(self):
|
||||||
|
"""Test default ResultData values."""
|
||||||
|
data = ResultData()
|
||||||
|
|
||||||
|
self.assertEqual(data.score, 0)
|
||||||
|
self.assertEqual(data.good, 0)
|
||||||
|
self.assertEqual(data.ok, 0)
|
||||||
|
self.assertEqual(data.bad, 0)
|
||||||
|
self.assertEqual(data.max_combo, 0)
|
||||||
|
self.assertEqual(data.total_drumroll, 0)
|
||||||
|
self.assertEqual(data.gauge_length, 0)
|
||||||
|
self.assertEqual(data.prev_score, 0)
|
||||||
|
|
||||||
|
def test_custom_values(self):
|
||||||
|
"""Test custom ResultData values."""
|
||||||
|
data = ResultData(
|
||||||
|
score=500000,
|
||||||
|
good=150,
|
||||||
|
ok=30,
|
||||||
|
bad=10,
|
||||||
|
max_combo=120,
|
||||||
|
total_drumroll=45,
|
||||||
|
gauge_length=0.85,
|
||||||
|
prev_score=450000
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(data.score, 500000)
|
||||||
|
self.assertEqual(data.good, 150)
|
||||||
|
self.assertEqual(data.ok, 30)
|
||||||
|
self.assertEqual(data.bad, 10)
|
||||||
|
self.assertEqual(data.max_combo, 120)
|
||||||
|
self.assertEqual(data.total_drumroll, 45)
|
||||||
|
self.assertEqual(data.gauge_length, 0.85)
|
||||||
|
self.assertEqual(data.prev_score, 450000)
|
||||||
|
|
||||||
|
def test_total_notes(self):
|
||||||
|
"""Test calculating total notes from result data."""
|
||||||
|
data = ResultData(good=100, ok=50, bad=10)
|
||||||
|
total = data.good + data.ok + data.bad
|
||||||
|
|
||||||
|
self.assertEqual(total, 160)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSessionData(unittest.TestCase):
|
||||||
|
"""Test cases for the SessionData dataclass."""
|
||||||
|
|
||||||
|
def test_default_values(self):
|
||||||
|
"""Test default SessionData values."""
|
||||||
|
session = SessionData()
|
||||||
|
|
||||||
|
self.assertEqual(session.selected_song, Path())
|
||||||
|
self.assertEqual(session.song_hash, "")
|
||||||
|
self.assertEqual(session.selected_dan, [])
|
||||||
|
self.assertEqual(session.selected_dan_exam, [])
|
||||||
|
self.assertEqual(session.dan_color, 0)
|
||||||
|
self.assertEqual(session.selected_difficulty, 0)
|
||||||
|
self.assertEqual(session.song_title, "default_title")
|
||||||
|
self.assertEqual(session.genre_index, 0)
|
||||||
|
self.assertIsInstance(session.result_data, ResultData)
|
||||||
|
self.assertIsInstance(session.dan_result_data, DanResultData)
|
||||||
|
|
||||||
|
def test_custom_song_selection(self):
|
||||||
|
"""Test custom song selection."""
|
||||||
|
song_path = Path("Songs/TestSong/song.tja")
|
||||||
|
session = SessionData(
|
||||||
|
selected_song=song_path,
|
||||||
|
song_hash="abc123",
|
||||||
|
selected_difficulty=3,
|
||||||
|
song_title="Test Song"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(session.selected_song, song_path)
|
||||||
|
self.assertEqual(session.song_hash, "abc123")
|
||||||
|
self.assertEqual(session.selected_difficulty, 3)
|
||||||
|
self.assertEqual(session.song_title, "Test Song")
|
||||||
|
|
||||||
|
def test_dan_selection(self):
|
||||||
|
"""Test dan course selection."""
|
||||||
|
dan_songs = [(Mock(), 0, 3, 10), (Mock(), 1, 3, 10)]
|
||||||
|
dan_exams = [Mock(), Mock(), Mock()]
|
||||||
|
|
||||||
|
session = SessionData(
|
||||||
|
selected_dan=dan_songs,
|
||||||
|
selected_dan_exam=dan_exams,
|
||||||
|
dan_color=2
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(session.selected_dan), 2)
|
||||||
|
self.assertEqual(len(session.selected_dan_exam), 3)
|
||||||
|
self.assertEqual(session.dan_color, 2)
|
||||||
|
|
||||||
|
def test_result_data_independence(self):
|
||||||
|
"""Test that each session has independent result data."""
|
||||||
|
session1 = SessionData()
|
||||||
|
session2 = SessionData()
|
||||||
|
|
||||||
|
session1.result_data.score = 100000
|
||||||
|
|
||||||
|
self.assertEqual(session1.result_data.score, 100000)
|
||||||
|
self.assertEqual(session2.result_data.score, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCamera(unittest.TestCase):
|
||||||
|
"""Test cases for the Camera class."""
|
||||||
|
|
||||||
|
@patch('libs.global_data.ray')
|
||||||
|
def test_default_values(self, mock_ray):
|
||||||
|
"""Test default Camera values."""
|
||||||
|
mock_ray.Vector2 = Mock(return_value=Mock())
|
||||||
|
mock_ray.BLACK = Mock()
|
||||||
|
|
||||||
|
camera = Camera()
|
||||||
|
|
||||||
|
self.assertEqual(camera.zoom, 1.0)
|
||||||
|
self.assertEqual(camera.h_scale, 1.0)
|
||||||
|
self.assertEqual(camera.v_scale, 1.0)
|
||||||
|
self.assertEqual(camera.rotation, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGlobalData(unittest.TestCase):
|
||||||
|
"""Test cases for the GlobalData dataclass."""
|
||||||
|
|
||||||
|
def test_default_values(self):
|
||||||
|
"""Test default GlobalData values."""
|
||||||
|
data = GlobalData()
|
||||||
|
|
||||||
|
self.assertEqual(data.songs_played, 0)
|
||||||
|
self.assertIsInstance(data.camera, Camera)
|
||||||
|
self.assertEqual(data.song_hashes, {})
|
||||||
|
self.assertEqual(data.song_paths, {})
|
||||||
|
self.assertEqual(data.score_db, "")
|
||||||
|
self.assertEqual(data.song_progress, 0.0)
|
||||||
|
self.assertEqual(data.total_songs, 0)
|
||||||
|
self.assertEqual(data.hit_sound, [0, 0, 0])
|
||||||
|
self.assertEqual(data.player_num, PlayerNum.P1)
|
||||||
|
self.assertEqual(data.input_locked, 0)
|
||||||
|
|
||||||
|
def test_modifiers_list(self):
|
||||||
|
"""Test that modifiers list has correct size."""
|
||||||
|
data = GlobalData()
|
||||||
|
|
||||||
|
self.assertEqual(len(data.modifiers), 3)
|
||||||
|
self.assertIsInstance(data.modifiers[0], Modifiers)
|
||||||
|
self.assertIsInstance(data.modifiers[1], Modifiers)
|
||||||
|
self.assertIsInstance(data.modifiers[2], Modifiers)
|
||||||
|
|
||||||
|
def test_session_data_list(self):
|
||||||
|
"""Test that session data list has correct size."""
|
||||||
|
data = GlobalData()
|
||||||
|
|
||||||
|
self.assertEqual(len(data.session_data), 3)
|
||||||
|
self.assertIsInstance(data.session_data[0], SessionData)
|
||||||
|
self.assertIsInstance(data.session_data[1], SessionData)
|
||||||
|
self.assertIsInstance(data.session_data[2], SessionData)
|
||||||
|
|
||||||
|
def test_song_hashes_dict(self):
|
||||||
|
"""Test song_hashes dictionary operations."""
|
||||||
|
data = GlobalData()
|
||||||
|
|
||||||
|
data.song_hashes["hash1"] = [{"path": "Songs/Song1"}]
|
||||||
|
data.song_hashes["hash2"] = [{"path": "Songs/Song2"}]
|
||||||
|
|
||||||
|
self.assertEqual(len(data.song_hashes), 2)
|
||||||
|
self.assertIn("hash1", data.song_hashes)
|
||||||
|
self.assertIn("hash2", data.song_hashes)
|
||||||
|
|
||||||
|
def test_song_paths_dict(self):
|
||||||
|
"""Test song_paths dictionary operations."""
|
||||||
|
data = GlobalData()
|
||||||
|
|
||||||
|
path1 = Path("Songs/Song1/song.tja")
|
||||||
|
path2 = Path("Songs/Song2/song.tja")
|
||||||
|
|
||||||
|
data.song_paths[path1] = "hash1"
|
||||||
|
data.song_paths[path2] = "hash2"
|
||||||
|
|
||||||
|
self.assertEqual(len(data.song_paths), 2)
|
||||||
|
self.assertEqual(data.song_paths[path1], "hash1")
|
||||||
|
self.assertEqual(data.song_paths[path2], "hash2")
|
||||||
|
|
||||||
|
def test_input_locked_counter(self):
|
||||||
|
"""Test input_locked as a counter."""
|
||||||
|
data = GlobalData()
|
||||||
|
|
||||||
|
self.assertEqual(data.input_locked, 0)
|
||||||
|
|
||||||
|
data.input_locked += 1
|
||||||
|
self.assertEqual(data.input_locked, 1)
|
||||||
|
|
||||||
|
data.input_locked += 1
|
||||||
|
self.assertEqual(data.input_locked, 2)
|
||||||
|
|
||||||
|
data.input_locked -= 1
|
||||||
|
self.assertEqual(data.input_locked, 1)
|
||||||
|
|
||||||
|
def test_songs_played_counter(self):
|
||||||
|
"""Test songs_played counter."""
|
||||||
|
data = GlobalData()
|
||||||
|
|
||||||
|
self.assertEqual(data.songs_played, 0)
|
||||||
|
|
||||||
|
data.songs_played += 1
|
||||||
|
self.assertEqual(data.songs_played, 1)
|
||||||
|
|
||||||
|
data.songs_played += 1
|
||||||
|
self.assertEqual(data.songs_played, 2)
|
||||||
|
|
||||||
|
def test_hit_sound_indices(self):
|
||||||
|
"""Test hit_sound indices list."""
|
||||||
|
data = GlobalData()
|
||||||
|
|
||||||
|
self.assertEqual(data.hit_sound, [0, 0, 0])
|
||||||
|
|
||||||
|
data.hit_sound[0] = 1
|
||||||
|
data.hit_sound[1] = 2
|
||||||
|
data.hit_sound[2] = 3
|
||||||
|
|
||||||
|
self.assertEqual(data.hit_sound, [1, 2, 3])
|
||||||
|
|
||||||
|
|
||||||
|
class TestGlobalDataSingleton(unittest.TestCase):
|
||||||
|
"""Test cases for the global_data singleton."""
|
||||||
|
|
||||||
|
def test_global_data_exists(self):
|
||||||
|
"""Test that global_data instance exists."""
|
||||||
|
self.assertIsInstance(global_data, GlobalData)
|
||||||
|
|
||||||
|
def test_global_data_modifiable(self):
|
||||||
|
"""Test that global_data can be modified."""
|
||||||
|
original_songs_played = global_data.songs_played
|
||||||
|
global_data.songs_played += 1
|
||||||
|
|
||||||
|
self.assertEqual(global_data.songs_played, original_songs_played + 1)
|
||||||
|
|
||||||
|
# Reset for other tests
|
||||||
|
global_data.songs_played = original_songs_played
|
||||||
|
|
||||||
|
|
||||||
|
class TestResetSession(unittest.TestCase):
|
||||||
|
"""Test cases for reset_session function."""
|
||||||
|
|
||||||
|
def test_reset_session_clears_p1_data(self):
|
||||||
|
"""Test that reset_session clears player 1 data."""
|
||||||
|
global_data.session_data[1].result_data.score = 100000
|
||||||
|
global_data.session_data[1].song_title = "Test Song"
|
||||||
|
|
||||||
|
reset_session()
|
||||||
|
|
||||||
|
self.assertIsInstance(global_data.session_data[1], SessionData)
|
||||||
|
self.assertEqual(global_data.session_data[1].song_title, "default_title")
|
||||||
|
|
||||||
|
def test_reset_session_clears_p2_data(self):
|
||||||
|
"""Test that reset_session clears player 2 data."""
|
||||||
|
global_data.session_data[2].result_data.score = 50000
|
||||||
|
global_data.session_data[2].selected_difficulty = 3
|
||||||
|
|
||||||
|
reset_session()
|
||||||
|
|
||||||
|
self.assertIsInstance(global_data.session_data[2], SessionData)
|
||||||
|
self.assertEqual(global_data.session_data[2].selected_difficulty, 0)
|
||||||
|
|
||||||
|
def test_reset_session_preserves_index_0(self):
|
||||||
|
"""Test that reset_session doesn't affect index 0."""
|
||||||
|
original_data = global_data.session_data[0]
|
||||||
|
original_data.song_title = "Should Not Change"
|
||||||
|
|
||||||
|
reset_session()
|
||||||
|
|
||||||
|
self.assertEqual(global_data.session_data[0].song_title, "Should Not Change")
|
||||||
|
|
||||||
|
def test_reset_session_creates_new_instances(self):
|
||||||
|
"""Test that reset_session creates new SessionData instances."""
|
||||||
|
old_p1_session = global_data.session_data[1]
|
||||||
|
old_p2_session = global_data.session_data[2]
|
||||||
|
|
||||||
|
reset_session()
|
||||||
|
|
||||||
|
self.assertIsNot(global_data.session_data[1], old_p1_session)
|
||||||
|
self.assertIsNot(global_data.session_data[2], old_p2_session)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDataclassIntegration(unittest.TestCase):
|
||||||
|
"""Integration tests for dataclass interactions."""
|
||||||
|
|
||||||
|
def test_session_with_result_data(self):
|
||||||
|
"""Test SessionData with populated ResultData."""
|
||||||
|
session = SessionData()
|
||||||
|
session.result_data.score = 750000
|
||||||
|
session.result_data.good = 200
|
||||||
|
session.result_data.max_combo = 180
|
||||||
|
|
||||||
|
self.assertEqual(session.result_data.score, 750000)
|
||||||
|
self.assertEqual(session.result_data.good, 200)
|
||||||
|
self.assertEqual(session.result_data.max_combo, 180)
|
||||||
|
|
||||||
|
def test_session_with_dan_result_data(self):
|
||||||
|
"""Test SessionData with populated DanResultData."""
|
||||||
|
session = SessionData()
|
||||||
|
session.dan_result_data.dan_title = "10th Dan"
|
||||||
|
session.dan_result_data.dan_color = 5
|
||||||
|
|
||||||
|
song1 = DanResultSong(song_title="Dan Song 1")
|
||||||
|
song2 = DanResultSong(song_title="Dan Song 2")
|
||||||
|
session.dan_result_data.songs = [song1, song2]
|
||||||
|
|
||||||
|
self.assertEqual(session.dan_result_data.dan_title, "10th Dan")
|
||||||
|
self.assertEqual(len(session.dan_result_data.songs), 2)
|
||||||
|
|
||||||
|
def test_modifiers_independent_per_player(self):
|
||||||
|
"""Test that each player has independent modifiers."""
|
||||||
|
data = GlobalData()
|
||||||
|
|
||||||
|
data.modifiers[1].speed = 2.0
|
||||||
|
data.modifiers[2].speed = 1.5
|
||||||
|
|
||||||
|
self.assertEqual(data.modifiers[1].speed, 2.0)
|
||||||
|
self.assertEqual(data.modifiers[2].speed, 1.5)
|
||||||
|
self.assertEqual(data.modifiers[0].speed, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
289
test/libs/test_global_objects.py
Normal file
289
test/libs/test_global_objects.py
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from libs.global_data import PlayerNum
|
||||||
|
from libs.global_objects import (
|
||||||
|
AllNetIcon,
|
||||||
|
CoinOverlay,
|
||||||
|
EntryOverlay,
|
||||||
|
Indicator,
|
||||||
|
Nameplate,
|
||||||
|
Timer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNameplate(unittest.TestCase):
|
||||||
|
"""Test cases for the Nameplate class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
# Mock global_tex and its methods
|
||||||
|
self.mock_tex = Mock()
|
||||||
|
self.mock_tex.skin_config = {
|
||||||
|
"nameplate_text_name": Mock(font_size=20, x=100, y=50, width=200),
|
||||||
|
"nameplate_text_title": Mock(font_size=16, x=100, y=80, width=150),
|
||||||
|
"nameplate_title_offset": Mock(x=10),
|
||||||
|
"nameplate_dan_offset": Mock(x=20)
|
||||||
|
}
|
||||||
|
self.mock_tex.get_animation = Mock(return_value=Mock(start=Mock(), update=Mock(), is_finished=False))
|
||||||
|
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
@patch('libs.global_objects.OutlinedText')
|
||||||
|
def test_initialization_basic(self, mock_text, mock_global_tex):
|
||||||
|
"""Test basic nameplate initialization."""
|
||||||
|
mock_global_tex.skin_config = self.mock_tex.skin_config
|
||||||
|
|
||||||
|
nameplate = Nameplate("TestPlayer", "TestTitle", PlayerNum.P1, 5, False, False, 0)
|
||||||
|
|
||||||
|
self.assertEqual(nameplate.dan_index, 5)
|
||||||
|
self.assertEqual(nameplate.player_num, 1)
|
||||||
|
self.assertFalse(nameplate.is_gold)
|
||||||
|
self.assertFalse(nameplate.is_rainbow)
|
||||||
|
self.assertEqual(nameplate.title_bg, 0)
|
||||||
|
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
@patch('libs.global_objects.OutlinedText')
|
||||||
|
def test_initialization_rainbow(self, mock_text, mock_global_tex):
|
||||||
|
"""Test rainbow nameplate initialization."""
|
||||||
|
mock_global_tex.skin_config = self.mock_tex.skin_config
|
||||||
|
mock_animation = Mock()
|
||||||
|
mock_global_tex.get_animation.return_value = mock_animation
|
||||||
|
|
||||||
|
nameplate = Nameplate("Player", "Title", PlayerNum.P1, 3, False, True, 0)
|
||||||
|
|
||||||
|
self.assertTrue(nameplate.is_rainbow)
|
||||||
|
mock_global_tex.get_animation.assert_called_once_with(12)
|
||||||
|
mock_animation.start.assert_called_once()
|
||||||
|
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
def test_update_rainbow_animation(self, mock_global_tex):
|
||||||
|
"""Test rainbow animation update logic."""
|
||||||
|
mock_animation = Mock(is_finished=False, update=Mock())
|
||||||
|
mock_global_tex.get_animation.return_value = mock_animation
|
||||||
|
mock_global_tex.skin_config = self.mock_tex.skin_config
|
||||||
|
|
||||||
|
with patch('libs.global_objects.OutlinedText'):
|
||||||
|
nameplate = Nameplate("P", "T", PlayerNum.P1, 0, False, True, 0)
|
||||||
|
nameplate.update(1000.0)
|
||||||
|
|
||||||
|
mock_animation.update.assert_called_once_with(1000.0)
|
||||||
|
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
def test_update_rainbow_restart(self, mock_global_tex):
|
||||||
|
"""Test rainbow animation restarts when finished."""
|
||||||
|
mock_animation = Mock(is_finished=True, update=Mock(), restart=Mock())
|
||||||
|
mock_global_tex.get_animation.return_value = mock_animation
|
||||||
|
mock_global_tex.skin_config = self.mock_tex.skin_config
|
||||||
|
|
||||||
|
with patch('libs.global_objects.OutlinedText'):
|
||||||
|
nameplate = Nameplate("P", "T", PlayerNum.P1, 0, False, True, 0)
|
||||||
|
nameplate.update(1000.0)
|
||||||
|
|
||||||
|
mock_animation.restart.assert_called_once()
|
||||||
|
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
def test_unload(self, mock_global_tex):
|
||||||
|
"""Test nameplate resource cleanup."""
|
||||||
|
mock_global_tex.skin_config = self.mock_tex.skin_config
|
||||||
|
|
||||||
|
with patch('libs.global_objects.OutlinedText') as mock_text:
|
||||||
|
mock_name = Mock()
|
||||||
|
mock_title = Mock()
|
||||||
|
mock_text.side_effect = [mock_name, mock_title]
|
||||||
|
|
||||||
|
nameplate = Nameplate("P", "T", PlayerNum.P1, 0, False, False, 0)
|
||||||
|
nameplate.unload()
|
||||||
|
|
||||||
|
mock_name.unload.assert_called_once()
|
||||||
|
mock_title.unload.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestIndicator(unittest.TestCase):
|
||||||
|
"""Test cases for the Indicator class."""
|
||||||
|
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
@patch('libs.global_objects.OutlinedText')
|
||||||
|
def test_initialization(self, mock_text, mock_global_tex):
|
||||||
|
"""Test indicator initialization with different states."""
|
||||||
|
mock_global_tex.get_animation.return_value = Mock()
|
||||||
|
mock_global_tex.skin_config = {"indicator_text": Mock(text={"en": "Select"}, font_size=20)}
|
||||||
|
|
||||||
|
with patch('libs.global_objects.global_data') as mock_data:
|
||||||
|
mock_data.config = {"general": {"language": "en"}}
|
||||||
|
|
||||||
|
indicator = Indicator(Indicator.State.SELECT)
|
||||||
|
|
||||||
|
self.assertEqual(indicator.state, Indicator.State.SELECT)
|
||||||
|
self.assertEqual(mock_global_tex.get_animation.call_count, 3)
|
||||||
|
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
def test_update_animations(self, mock_global_tex):
|
||||||
|
"""Test that all animations update correctly."""
|
||||||
|
mock_don_fade = Mock()
|
||||||
|
mock_arrow_move = Mock()
|
||||||
|
mock_arrow_fade = Mock()
|
||||||
|
mock_global_tex.get_animation.side_effect = [mock_don_fade, mock_arrow_move, mock_arrow_fade]
|
||||||
|
mock_global_tex.skin_config = {"indicator_text": Mock(text={"en": "S"}, font_size=20)}
|
||||||
|
|
||||||
|
with patch('libs.global_objects.global_data.config', {"general": {"language": "en"}}):
|
||||||
|
with patch('libs.global_objects.OutlinedText'):
|
||||||
|
indicator = Indicator(Indicator.State.SKIP)
|
||||||
|
indicator.update(500.0)
|
||||||
|
|
||||||
|
mock_don_fade.update.assert_called_once_with(500.0)
|
||||||
|
mock_arrow_move.update.assert_called_once_with(500.0)
|
||||||
|
mock_arrow_fade.update.assert_called_once_with(500.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimer(unittest.TestCase):
|
||||||
|
"""Test cases for the Timer class."""
|
||||||
|
|
||||||
|
@patch('libs.global_objects.get_config')
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
def test_initialization(self, mock_tex, mock_config):
|
||||||
|
"""Test timer initialization."""
|
||||||
|
mock_config.return_value = {"general": {"timer_frozen": False}}
|
||||||
|
mock_tex.get_animation.return_value = Mock()
|
||||||
|
mock_func = Mock()
|
||||||
|
|
||||||
|
timer = Timer(30, 0.0, mock_func)
|
||||||
|
|
||||||
|
self.assertEqual(timer.time, 30)
|
||||||
|
self.assertEqual(timer.counter, "30")
|
||||||
|
self.assertFalse(timer.is_finished)
|
||||||
|
self.assertFalse(timer.is_frozen)
|
||||||
|
|
||||||
|
@patch('libs.global_objects.audio')
|
||||||
|
@patch('libs.global_objects.get_config')
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
def test_countdown_normal(self, mock_tex, mock_config, mock_audio):
|
||||||
|
"""Test normal countdown behavior."""
|
||||||
|
mock_config.return_value = {"general": {"timer_frozen": False}}
|
||||||
|
mock_tex.get_animation.return_value = Mock(update=Mock(), start=Mock())
|
||||||
|
mock_func = Mock()
|
||||||
|
|
||||||
|
timer = Timer(15, 0.0, mock_func)
|
||||||
|
timer.update(1000.0)
|
||||||
|
|
||||||
|
self.assertEqual(timer.time, 14)
|
||||||
|
self.assertEqual(timer.counter, "14")
|
||||||
|
|
||||||
|
@patch('libs.global_objects.audio')
|
||||||
|
@patch('libs.global_objects.get_config')
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
def test_countdown_below_ten(self, mock_tex, mock_config, mock_audio):
|
||||||
|
"""Test countdown triggers animations below 10."""
|
||||||
|
mock_config.return_value = {"general": {"timer_frozen": False}}
|
||||||
|
mock_animation = Mock(update=Mock(), start=Mock())
|
||||||
|
mock_tex.get_animation.return_value = mock_animation
|
||||||
|
mock_func = Mock()
|
||||||
|
|
||||||
|
timer = Timer(10, 0.0, mock_func)
|
||||||
|
timer.update(1000.0)
|
||||||
|
|
||||||
|
self.assertEqual(timer.time, 9)
|
||||||
|
mock_audio.play_sound.assert_called_with('timer_blip', 'sound')
|
||||||
|
self.assertEqual(mock_animation.start.call_count, 3)
|
||||||
|
|
||||||
|
@patch('libs.global_objects.audio')
|
||||||
|
@patch('libs.global_objects.get_config')
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
def test_voice_triggers(self, mock_tex, mock_config, mock_audio):
|
||||||
|
"""Test voice announcements at specific times."""
|
||||||
|
mock_config.return_value = {"general": {"timer_frozen": False}}
|
||||||
|
mock_tex.get_animation.return_value = Mock(update=Mock(), start=Mock())
|
||||||
|
mock_func = Mock()
|
||||||
|
|
||||||
|
# Test 10 second voice
|
||||||
|
timer = Timer(11, 0.0, mock_func)
|
||||||
|
timer.update(1000.0)
|
||||||
|
mock_audio.play_sound.assert_called_with('voice_timer_10', 'voice')
|
||||||
|
|
||||||
|
# Test 5 second voice
|
||||||
|
timer = Timer(6, 0.0, mock_func)
|
||||||
|
timer.update(1000.0)
|
||||||
|
mock_audio.play_sound.assert_called_with('voice_timer_5', 'voice')
|
||||||
|
|
||||||
|
@patch('libs.global_objects.audio')
|
||||||
|
@patch('libs.global_objects.get_config')
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
def test_timer_finish_callback(self, mock_tex, mock_config, mock_audio):
|
||||||
|
"""Test callback is triggered when timer reaches zero."""
|
||||||
|
mock_config.return_value = {"general": {"timer_frozen": False}}
|
||||||
|
mock_tex.get_animation.return_value = Mock(update=Mock(), start=Mock())
|
||||||
|
mock_audio.is_sound_playing.return_value = False
|
||||||
|
mock_func = Mock()
|
||||||
|
|
||||||
|
timer = Timer(1, 0.0, mock_func)
|
||||||
|
timer.update(1000.0)
|
||||||
|
timer.update(2000.0)
|
||||||
|
|
||||||
|
mock_func.assert_called_once()
|
||||||
|
self.assertTrue(timer.is_finished)
|
||||||
|
|
||||||
|
@patch('libs.global_objects.get_config')
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
def test_timer_frozen(self, mock_tex, mock_config):
|
||||||
|
"""Test frozen timer doesn't count down."""
|
||||||
|
mock_config.return_value = {"general": {"timer_frozen": True}}
|
||||||
|
mock_tex.get_animation.return_value = Mock(update=Mock())
|
||||||
|
mock_func = Mock()
|
||||||
|
|
||||||
|
timer = Timer(10, 0.0, mock_func)
|
||||||
|
initial_time = timer.time
|
||||||
|
timer.update(1000.0)
|
||||||
|
|
||||||
|
self.assertEqual(timer.time, initial_time)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoinOverlay(unittest.TestCase):
|
||||||
|
"""Test cases for the CoinOverlay class."""
|
||||||
|
|
||||||
|
@patch('libs.global_objects.global_tex')
|
||||||
|
@patch('libs.global_objects.global_data')
|
||||||
|
@patch('libs.global_objects.OutlinedText')
|
||||||
|
def test_initialization(self, mock_text, mock_data, mock_tex):
|
||||||
|
"""Test coin overlay initialization."""
|
||||||
|
mock_tex.skin_config = {
|
||||||
|
"free_play": Mock(text={"en": "Free Play"}, font_size=24, y=100)
|
||||||
|
}
|
||||||
|
mock_data.config = {"general": {"language": "en"}}
|
||||||
|
|
||||||
|
_ = CoinOverlay()
|
||||||
|
|
||||||
|
mock_text.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAllNetIcon(unittest.TestCase):
|
||||||
|
"""Test cases for the AllNetIcon class."""
|
||||||
|
|
||||||
|
@patch('libs.global_objects.get_config')
|
||||||
|
def test_initialization_offline(self, mock_config):
|
||||||
|
"""Test AllNet icon initializes offline."""
|
||||||
|
mock_config.return_value = {"general": {"fake_online": False}}
|
||||||
|
|
||||||
|
icon = AllNetIcon()
|
||||||
|
|
||||||
|
self.assertFalse(icon.online)
|
||||||
|
|
||||||
|
@patch('libs.global_objects.get_config')
|
||||||
|
def test_initialization_online(self, mock_config):
|
||||||
|
"""Test AllNet icon initializes online."""
|
||||||
|
mock_config.return_value = {"general": {"fake_online": True}}
|
||||||
|
|
||||||
|
icon = AllNetIcon()
|
||||||
|
|
||||||
|
self.assertTrue(icon.online)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEntryOverlay(unittest.TestCase):
|
||||||
|
"""Test cases for the EntryOverlay class."""
|
||||||
|
|
||||||
|
@patch('libs.global_objects.get_config')
|
||||||
|
def test_initialization(self, mock_config):
|
||||||
|
"""Test entry overlay initialization."""
|
||||||
|
mock_config.return_value = {"general": {"fake_online": False}}
|
||||||
|
|
||||||
|
overlay = EntryOverlay()
|
||||||
|
|
||||||
|
self.assertFalse(overlay.online)
|
||||||
333
test/libs/test_screen.py
Normal file
333
test/libs/test_screen.py
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from libs.screen import Screen
|
||||||
|
|
||||||
|
|
||||||
|
class TestScreen(unittest.TestCase):
|
||||||
|
"""Test cases for the Screen class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.screen_name = "test_screen"
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_initialization(self, mock_audio, mock_tex):
|
||||||
|
"""Test screen initialization."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
|
||||||
|
self.assertEqual(screen.screen_name, self.screen_name)
|
||||||
|
self.assertFalse(screen.screen_init)
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_on_screen_start(self, mock_audio, mock_tex):
|
||||||
|
"""Test on_screen_start loads textures and sounds."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
screen.on_screen_start()
|
||||||
|
|
||||||
|
mock_tex.load_screen_textures.assert_called_once_with(self.screen_name)
|
||||||
|
mock_audio.load_screen_sounds.assert_called_once_with(self.screen_name)
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_do_screen_start_first_call(self, mock_audio, mock_tex):
|
||||||
|
"""Test _do_screen_start initializes screen on first call."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
|
||||||
|
self.assertFalse(screen.screen_init)
|
||||||
|
screen._do_screen_start()
|
||||||
|
|
||||||
|
self.assertTrue(screen.screen_init)
|
||||||
|
mock_tex.load_screen_textures.assert_called_once_with(self.screen_name)
|
||||||
|
mock_audio.load_screen_sounds.assert_called_once_with(self.screen_name)
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_do_screen_start_subsequent_calls(self, mock_audio, mock_tex):
|
||||||
|
"""Test _do_screen_start doesn't reinitialize on subsequent calls."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
|
||||||
|
screen._do_screen_start()
|
||||||
|
screen._do_screen_start()
|
||||||
|
screen._do_screen_start()
|
||||||
|
|
||||||
|
# Should only be called once despite multiple calls
|
||||||
|
mock_tex.load_screen_textures.assert_called_once()
|
||||||
|
mock_audio.load_screen_sounds.assert_called_once()
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_on_screen_end(self, mock_audio, mock_tex):
|
||||||
|
"""Test on_screen_end unloads resources and returns next screen."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
screen.screen_init = True
|
||||||
|
|
||||||
|
next_screen = "next_screen"
|
||||||
|
result = screen.on_screen_end(next_screen)
|
||||||
|
|
||||||
|
self.assertEqual(result, next_screen)
|
||||||
|
self.assertFalse(screen.screen_init)
|
||||||
|
mock_audio.unload_all_sounds.assert_called_once()
|
||||||
|
mock_audio.unload_all_music.assert_called_once()
|
||||||
|
mock_tex.unload_textures.assert_called_once()
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_on_screen_end_unload_order(self, mock_audio, mock_tex):
|
||||||
|
"""Test that resources are unloaded in correct order."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
screen.screen_init = True
|
||||||
|
|
||||||
|
manager = Mock()
|
||||||
|
manager.attach_mock(mock_audio.unload_all_sounds, 'unload_sounds')
|
||||||
|
manager.attach_mock(mock_audio.unload_all_music, 'unload_music')
|
||||||
|
manager.attach_mock(mock_tex.unload_textures, 'unload_textures')
|
||||||
|
|
||||||
|
screen.on_screen_end("next")
|
||||||
|
|
||||||
|
# Verify order: sounds, music, then textures
|
||||||
|
calls = manager.mock_calls
|
||||||
|
self.assertEqual(calls[0][0], 'unload_sounds')
|
||||||
|
self.assertEqual(calls[1][0], 'unload_music')
|
||||||
|
self.assertEqual(calls[2][0], 'unload_textures')
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_update_not_initialized(self, mock_audio, mock_tex):
|
||||||
|
"""Test update initializes screen if not already initialized."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
|
||||||
|
self.assertFalse(screen.screen_init)
|
||||||
|
screen.update()
|
||||||
|
|
||||||
|
self.assertTrue(screen.screen_init)
|
||||||
|
mock_tex.load_screen_textures.assert_called_once()
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_update_already_initialized(self, mock_audio, mock_tex):
|
||||||
|
"""Test update doesn't reinitialize if already initialized."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
screen.screen_init = True
|
||||||
|
|
||||||
|
screen.update()
|
||||||
|
|
||||||
|
# Should not load again
|
||||||
|
mock_tex.load_screen_textures.assert_not_called()
|
||||||
|
mock_audio.load_screen_sounds.assert_not_called()
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_update_returns_value(self, mock_audio, mock_tex):
|
||||||
|
"""Test update returns value from _do_screen_start."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
|
||||||
|
with patch.object(screen, '_do_screen_start', return_value="test_value"):
|
||||||
|
result = screen.update()
|
||||||
|
self.assertEqual(result, "test_value")
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_draw_default_implementation(self, mock_audio, mock_tex):
|
||||||
|
"""Test draw has empty default implementation."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
|
||||||
|
# Should not raise any errors
|
||||||
|
screen.draw()
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_do_draw_when_initialized(self, mock_audio, mock_tex):
|
||||||
|
"""Test _do_draw calls draw when screen is initialized."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
screen.screen_init = True
|
||||||
|
|
||||||
|
with patch.object(screen, 'draw') as mock_draw:
|
||||||
|
screen._do_draw()
|
||||||
|
mock_draw.assert_called_once()
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_do_draw_when_not_initialized(self, mock_audio, mock_tex):
|
||||||
|
"""Test _do_draw doesn't call draw when screen is not initialized."""
|
||||||
|
screen = Screen(self.screen_name)
|
||||||
|
screen.screen_init = False
|
||||||
|
|
||||||
|
with patch.object(screen, 'draw') as mock_draw:
|
||||||
|
screen._do_draw()
|
||||||
|
mock_draw.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
class TestScreenSubclass(unittest.TestCase):
|
||||||
|
"""Test cases for Screen subclass behavior."""
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_subclass_custom_on_screen_start(self, mock_audio, mock_tex):
|
||||||
|
"""Test that subclass can override on_screen_start."""
|
||||||
|
class CustomScreen(Screen):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
self.custom_init_called = False
|
||||||
|
|
||||||
|
def on_screen_start(self):
|
||||||
|
super().on_screen_start()
|
||||||
|
self.custom_init_called = True
|
||||||
|
|
||||||
|
screen = CustomScreen("custom")
|
||||||
|
screen.on_screen_start()
|
||||||
|
|
||||||
|
self.assertTrue(screen.custom_init_called)
|
||||||
|
mock_tex.load_screen_textures.assert_called_once_with("custom")
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_subclass_custom_update(self, mock_audio, mock_tex):
|
||||||
|
"""Test that subclass can override update."""
|
||||||
|
class CustomScreen(Screen):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
self.update_count = 0
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
result = super().update()
|
||||||
|
self.update_count += 1
|
||||||
|
return result
|
||||||
|
|
||||||
|
screen = CustomScreen("custom")
|
||||||
|
screen.update()
|
||||||
|
screen.update()
|
||||||
|
|
||||||
|
self.assertEqual(screen.update_count, 2)
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_subclass_custom_draw(self, mock_audio, mock_tex):
|
||||||
|
"""Test that subclass can override draw."""
|
||||||
|
class CustomScreen(Screen):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
self.draw_called = False
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
self.draw_called = True
|
||||||
|
|
||||||
|
screen = CustomScreen("custom")
|
||||||
|
screen.screen_init = True
|
||||||
|
screen._do_draw()
|
||||||
|
|
||||||
|
self.assertTrue(screen.draw_called)
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_subclass_custom_on_screen_end(self, mock_audio, mock_tex):
|
||||||
|
"""Test that subclass can override on_screen_end."""
|
||||||
|
class CustomScreen(Screen):
|
||||||
|
def __init__(self, name):
|
||||||
|
super().__init__(name)
|
||||||
|
self.cleanup_called = False
|
||||||
|
|
||||||
|
def on_screen_end(self, next_screen):
|
||||||
|
self.cleanup_called = True
|
||||||
|
return super().on_screen_end(next_screen)
|
||||||
|
|
||||||
|
screen = CustomScreen("custom")
|
||||||
|
screen.screen_init = True
|
||||||
|
result = screen.on_screen_end("next")
|
||||||
|
|
||||||
|
self.assertTrue(screen.cleanup_called)
|
||||||
|
self.assertEqual(result, "next")
|
||||||
|
|
||||||
|
|
||||||
|
class TestScreenLifecycle(unittest.TestCase):
|
||||||
|
"""Test cases for complete screen lifecycle."""
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_full_lifecycle(self, mock_audio, mock_tex):
|
||||||
|
"""Test complete screen lifecycle from start to end."""
|
||||||
|
screen = Screen("lifecycle_test")
|
||||||
|
|
||||||
|
# Initial state
|
||||||
|
self.assertFalse(screen.screen_init)
|
||||||
|
|
||||||
|
# Start screen
|
||||||
|
screen.update()
|
||||||
|
self.assertTrue(screen.screen_init)
|
||||||
|
mock_tex.load_screen_textures.assert_called_once_with("lifecycle_test")
|
||||||
|
mock_audio.load_screen_sounds.assert_called_once_with("lifecycle_test")
|
||||||
|
|
||||||
|
# Multiple updates don't reinitialize
|
||||||
|
screen.update()
|
||||||
|
screen.update()
|
||||||
|
self.assertEqual(mock_tex.load_screen_textures.call_count, 1)
|
||||||
|
|
||||||
|
# Draw while initialized
|
||||||
|
with patch.object(screen, 'draw') as mock_draw:
|
||||||
|
screen._do_draw()
|
||||||
|
mock_draw.assert_called_once()
|
||||||
|
|
||||||
|
# End screen
|
||||||
|
result = screen.on_screen_end("next_screen")
|
||||||
|
self.assertEqual(result, "next_screen")
|
||||||
|
self.assertFalse(screen.screen_init)
|
||||||
|
mock_audio.unload_all_sounds.assert_called_once()
|
||||||
|
mock_audio.unload_all_music.assert_called_once()
|
||||||
|
mock_tex.unload_textures.assert_called_once()
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_multiple_screen_transitions(self, mock_audio, mock_tex):
|
||||||
|
"""Test transitioning between multiple screens."""
|
||||||
|
screen1 = Screen("screen1")
|
||||||
|
screen2 = Screen("screen2")
|
||||||
|
screen3 = Screen("screen3")
|
||||||
|
|
||||||
|
# Initialize first screen
|
||||||
|
screen1.update()
|
||||||
|
self.assertTrue(screen1.screen_init)
|
||||||
|
|
||||||
|
# Transition to second screen
|
||||||
|
next_name = screen1.on_screen_end("screen2")
|
||||||
|
self.assertEqual(next_name, "screen2")
|
||||||
|
self.assertFalse(screen1.screen_init)
|
||||||
|
|
||||||
|
screen2.update()
|
||||||
|
self.assertTrue(screen2.screen_init)
|
||||||
|
|
||||||
|
# Transition to third screen
|
||||||
|
next_name = screen2.on_screen_end("screen3")
|
||||||
|
self.assertEqual(next_name, "screen3")
|
||||||
|
self.assertFalse(screen2.screen_init)
|
||||||
|
|
||||||
|
screen3.update()
|
||||||
|
self.assertTrue(screen3.screen_init)
|
||||||
|
|
||||||
|
@patch('libs.screen.tex')
|
||||||
|
@patch('libs.screen.audio')
|
||||||
|
def test_screen_reinitialize_after_end(self, mock_audio, mock_tex):
|
||||||
|
"""Test that screen can be reinitialized after ending."""
|
||||||
|
screen = Screen("reinit_test")
|
||||||
|
|
||||||
|
# First initialization
|
||||||
|
screen.update()
|
||||||
|
self.assertTrue(screen.screen_init)
|
||||||
|
|
||||||
|
# End screen
|
||||||
|
screen.on_screen_end("next")
|
||||||
|
self.assertFalse(screen.screen_init)
|
||||||
|
|
||||||
|
# Reinitialize
|
||||||
|
mock_tex.load_screen_textures.reset_mock()
|
||||||
|
mock_audio.load_screen_sounds.reset_mock()
|
||||||
|
|
||||||
|
screen.update()
|
||||||
|
self.assertTrue(screen.screen_init)
|
||||||
|
mock_tex.load_screen_textures.assert_called_once()
|
||||||
|
mock_audio.load_screen_sounds.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
289
test/libs/test_texture.py
Normal file
289
test/libs/test_texture.py
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from libs.texture import (
|
||||||
|
FramedTexture,
|
||||||
|
SkinInfo,
|
||||||
|
Texture,
|
||||||
|
TextureWrapper,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSkinInfo(unittest.TestCase):
|
||||||
|
"""Test cases for the SkinInfo dataclass."""
|
||||||
|
|
||||||
|
def test_initialization(self):
|
||||||
|
"""Test SkinInfo initialization."""
|
||||||
|
skin_info = SkinInfo(
|
||||||
|
x=100.0,
|
||||||
|
y=200.0,
|
||||||
|
font_size=24,
|
||||||
|
width=300.0,
|
||||||
|
height=100.0,
|
||||||
|
text={"en": "Test", "ja": "テスト"}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(skin_info.x, 100.0)
|
||||||
|
self.assertEqual(skin_info.y, 200.0)
|
||||||
|
self.assertEqual(skin_info.font_size, 24)
|
||||||
|
self.assertEqual(skin_info.width, 300.0)
|
||||||
|
self.assertEqual(skin_info.height, 100.0)
|
||||||
|
self.assertEqual(skin_info.text, {"en": "Test", "ja": "テスト"})
|
||||||
|
|
||||||
|
def test_repr(self):
|
||||||
|
"""Test SkinInfo string representation."""
|
||||||
|
skin_info = SkinInfo(
|
||||||
|
x=100.0,
|
||||||
|
y=200.0,
|
||||||
|
font_size=24,
|
||||||
|
width=300.0,
|
||||||
|
height=100.0,
|
||||||
|
text={"en": "Test"}
|
||||||
|
)
|
||||||
|
|
||||||
|
repr_str = repr(skin_info)
|
||||||
|
self.assertIn("100.0", repr_str)
|
||||||
|
self.assertIn("200.0", repr_str)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTexture(unittest.TestCase):
|
||||||
|
"""Test cases for the Texture class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.mock_texture = Mock()
|
||||||
|
self.mock_texture.width = 100
|
||||||
|
self.mock_texture.height = 50
|
||||||
|
|
||||||
|
@patch('libs.texture.ray')
|
||||||
|
def test_initialization_single_texture(self, mock_ray):
|
||||||
|
"""Test Texture initialization with single texture."""
|
||||||
|
texture = Texture(
|
||||||
|
name="test_texture",
|
||||||
|
texture=self.mock_texture,
|
||||||
|
init_vals={}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(texture.name, "test_texture")
|
||||||
|
self.assertEqual(texture.texture, self.mock_texture)
|
||||||
|
self.assertEqual(texture.width, 100)
|
||||||
|
self.assertEqual(texture.height, 50)
|
||||||
|
self.assertEqual(texture.x, [0])
|
||||||
|
self.assertEqual(texture.y, [0])
|
||||||
|
|
||||||
|
@patch('libs.texture.ray')
|
||||||
|
def test_initialization_with_init_vals(self, mock_ray):
|
||||||
|
"""Test Texture initialization with init_vals."""
|
||||||
|
init_vals = {"x": 10, "y": 20}
|
||||||
|
texture = Texture(
|
||||||
|
name="test",
|
||||||
|
texture=self.mock_texture,
|
||||||
|
init_vals=init_vals
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(texture.init_vals, init_vals)
|
||||||
|
self.assertEqual(texture.name, "test")
|
||||||
|
|
||||||
|
@patch('libs.texture.ray')
|
||||||
|
def test_default_values(self, mock_ray):
|
||||||
|
"""Test Texture default values."""
|
||||||
|
texture = Texture(name="test", texture=self.mock_texture, init_vals={})
|
||||||
|
|
||||||
|
self.assertEqual(texture.x, [0])
|
||||||
|
self.assertEqual(texture.y, [0])
|
||||||
|
self.assertEqual(texture.x2, [100])
|
||||||
|
self.assertEqual(texture.y2, [50])
|
||||||
|
self.assertEqual(texture.controllable, [False])
|
||||||
|
|
||||||
|
@patch('libs.texture.ray')
|
||||||
|
def test_repr(self, mock_ray):
|
||||||
|
"""Test Texture string representation."""
|
||||||
|
texture = Texture(name="test", texture=self.mock_texture, init_vals={})
|
||||||
|
|
||||||
|
repr_str = repr(texture)
|
||||||
|
self.assertIn("test", repr_str)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFramedTexture(unittest.TestCase):
|
||||||
|
"""Test cases for the FramedTexture class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.mock_textures = [Mock() for _ in range(4)]
|
||||||
|
for tex in self.mock_textures:
|
||||||
|
tex.width = 200
|
||||||
|
tex.height = 100
|
||||||
|
|
||||||
|
@patch('libs.texture.ray')
|
||||||
|
def test_initialization(self, mock_ray):
|
||||||
|
"""Test FramedTexture initialization."""
|
||||||
|
framed = FramedTexture(
|
||||||
|
name="test_framed",
|
||||||
|
texture=self.mock_textures,
|
||||||
|
init_vals={}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(framed.name, "test_framed")
|
||||||
|
self.assertEqual(framed.texture, self.mock_textures)
|
||||||
|
self.assertEqual(framed.width, 200)
|
||||||
|
self.assertEqual(framed.height, 100)
|
||||||
|
self.assertEqual(framed.x, [0])
|
||||||
|
self.assertEqual(framed.y, [0])
|
||||||
|
|
||||||
|
@patch('libs.texture.ray')
|
||||||
|
def test_default_values(self, mock_ray):
|
||||||
|
"""Test FramedTexture default values."""
|
||||||
|
framed = FramedTexture(
|
||||||
|
name="test",
|
||||||
|
texture=self.mock_textures,
|
||||||
|
init_vals={}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(framed.x, [0])
|
||||||
|
self.assertEqual(framed.y, [0])
|
||||||
|
self.assertEqual(framed.x2, [200])
|
||||||
|
self.assertEqual(framed.y2, [100])
|
||||||
|
|
||||||
|
|
||||||
|
class TestTextureWrapper(unittest.TestCase):
|
||||||
|
"""Test cases for the TextureWrapper class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.mock_texture = Mock()
|
||||||
|
self.mock_texture.width = 100
|
||||||
|
self.mock_texture.height = 50
|
||||||
|
|
||||||
|
@patch('libs.texture.get_config')
|
||||||
|
@patch('libs.texture.Path')
|
||||||
|
def test_initialization(self, mock_path_cls, mock_get_config):
|
||||||
|
"""Test TextureWrapper initialization."""
|
||||||
|
mock_get_config.return_value = {'paths': {'skin': 'TestSkin'}}
|
||||||
|
|
||||||
|
# Mock the skin_config.json file
|
||||||
|
mock_path_instance = Mock()
|
||||||
|
mock_config_path = Mock()
|
||||||
|
mock_config_path.exists.return_value = True
|
||||||
|
mock_config_path.read_text.return_value = '{"screen": {"width": 1280, "height": 720}}'
|
||||||
|
mock_path_instance.__truediv__ = Mock(return_value=mock_config_path)
|
||||||
|
mock_path_cls.return_value = mock_path_instance
|
||||||
|
|
||||||
|
wrapper = TextureWrapper()
|
||||||
|
|
||||||
|
self.assertEqual(wrapper.screen_width, 1280)
|
||||||
|
self.assertEqual(wrapper.screen_height, 720)
|
||||||
|
self.assertIsInstance(wrapper.textures, dict)
|
||||||
|
|
||||||
|
@patch('libs.texture.get_config')
|
||||||
|
@patch('libs.texture.Path')
|
||||||
|
def test_get_animation(self, mock_path_cls, mock_get_config):
|
||||||
|
"""Test getting animation from list."""
|
||||||
|
mock_get_config.return_value = {'paths': {'skin': 'TestSkin'}}
|
||||||
|
|
||||||
|
# Mock the skin_config.json file
|
||||||
|
mock_path_instance = Mock()
|
||||||
|
mock_config_path = Mock()
|
||||||
|
mock_config_path.exists.return_value = True
|
||||||
|
mock_config_path.read_text.return_value = '{"screen": {"width": 1280, "height": 720}}'
|
||||||
|
mock_path_instance.__truediv__ = Mock(return_value=mock_config_path)
|
||||||
|
mock_path_cls.return_value = mock_path_instance
|
||||||
|
|
||||||
|
mock_animation = Mock()
|
||||||
|
|
||||||
|
wrapper = TextureWrapper()
|
||||||
|
wrapper.animations = {0: mock_animation}
|
||||||
|
|
||||||
|
result = wrapper.get_animation(0)
|
||||||
|
|
||||||
|
self.assertEqual(result, mock_animation)
|
||||||
|
|
||||||
|
@patch('libs.texture.get_config')
|
||||||
|
@patch('libs.texture.Path')
|
||||||
|
@patch('libs.texture.copy.deepcopy')
|
||||||
|
def test_get_animation_copy(self, mock_deepcopy, mock_path_cls, mock_get_config):
|
||||||
|
"""Test getting animation copy."""
|
||||||
|
mock_get_config.return_value = {'paths': {'skin': 'TestSkin'}}
|
||||||
|
|
||||||
|
# Mock the skin_config.json file
|
||||||
|
mock_path_instance = Mock()
|
||||||
|
mock_config_path = Mock()
|
||||||
|
mock_config_path.exists.return_value = True
|
||||||
|
mock_config_path.read_text.return_value = '{"screen": {"width": 1280, "height": 720}}'
|
||||||
|
mock_path_instance.__truediv__ = Mock(return_value=mock_config_path)
|
||||||
|
mock_path_cls.return_value = mock_path_instance
|
||||||
|
|
||||||
|
mock_animation = Mock()
|
||||||
|
mock_copy = Mock()
|
||||||
|
mock_deepcopy.return_value = mock_copy
|
||||||
|
|
||||||
|
wrapper = TextureWrapper()
|
||||||
|
wrapper.animations = {0: mock_animation}
|
||||||
|
|
||||||
|
result = wrapper.get_animation(0, is_copy=True)
|
||||||
|
|
||||||
|
mock_deepcopy.assert_called_once_with(mock_animation)
|
||||||
|
self.assertEqual(result, mock_copy)
|
||||||
|
|
||||||
|
@patch('libs.texture.get_config')
|
||||||
|
@patch('libs.texture.Path')
|
||||||
|
@patch('libs.texture.ray')
|
||||||
|
def test_read_tex_obj_data(self, mock_ray, mock_path_cls, mock_get_config):
|
||||||
|
"""Test reading texture object data from JSON."""
|
||||||
|
mock_get_config.return_value = {'paths': {'skin': 'TestSkin'}}
|
||||||
|
|
||||||
|
# Mock the skin_config.json file
|
||||||
|
mock_path_instance = Mock()
|
||||||
|
mock_config_path = Mock()
|
||||||
|
mock_config_path.exists.return_value = True
|
||||||
|
mock_config_path.read_text.return_value = '{"screen": {"width": 1280, "height": 720}}'
|
||||||
|
|
||||||
|
mock_path_instance.__truediv__ = Mock(return_value=mock_config_path)
|
||||||
|
mock_path_cls.return_value = mock_path_instance
|
||||||
|
|
||||||
|
wrapper = TextureWrapper()
|
||||||
|
|
||||||
|
# Create a mock texture object
|
||||||
|
mock_texture = Mock()
|
||||||
|
mock_texture.x = [0]
|
||||||
|
mock_texture.y = [0]
|
||||||
|
|
||||||
|
# Test with a dictionary mapping
|
||||||
|
tex_mapping = {"x": 10, "y": 20}
|
||||||
|
wrapper._read_tex_obj_data(tex_mapping, mock_texture)
|
||||||
|
|
||||||
|
# Verify the texture attributes were updated (they are lists)
|
||||||
|
self.assertEqual(mock_texture.x, [10])
|
||||||
|
self.assertEqual(mock_texture.y, [20])
|
||||||
|
|
||||||
|
@patch('libs.texture.get_config')
|
||||||
|
@patch('libs.texture.Path')
|
||||||
|
def test_read_tex_obj_data_not_exists(self, mock_path_cls, mock_get_config):
|
||||||
|
"""Test reading texture data with empty mapping."""
|
||||||
|
mock_get_config.return_value = {'paths': {'skin': 'TestSkin'}}
|
||||||
|
|
||||||
|
# Mock the skin_config.json file
|
||||||
|
mock_path_instance = Mock()
|
||||||
|
mock_config_path = Mock()
|
||||||
|
mock_config_path.exists.return_value = True
|
||||||
|
mock_config_path.read_text.return_value = '{"screen": {"width": 1280, "height": 720}}'
|
||||||
|
|
||||||
|
mock_path_instance.__truediv__ = Mock(return_value=mock_config_path)
|
||||||
|
mock_path_cls.return_value = mock_path_instance
|
||||||
|
|
||||||
|
wrapper = TextureWrapper()
|
||||||
|
|
||||||
|
# Create a mock texture object
|
||||||
|
mock_texture = Mock()
|
||||||
|
mock_texture.x = [0]
|
||||||
|
mock_texture.y = [0]
|
||||||
|
|
||||||
|
# Test with empty mapping (should not modify texture)
|
||||||
|
tex_mapping = {}
|
||||||
|
wrapper._read_tex_obj_data(tex_mapping, mock_texture)
|
||||||
|
|
||||||
|
# Verify the texture attributes remained unchanged
|
||||||
|
self.assertEqual(mock_texture.x, [0])
|
||||||
|
self.assertEqual(mock_texture.y, [0])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
67
uv.lock
generated
67
uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 2
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -83,6 +83,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nuitka"
|
name = "nuitka"
|
||||||
version = "2.8.4"
|
version = "2.8.4"
|
||||||
@@ -102,6 +120,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634, upload-time = "2022-01-26T14:38:48.677Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/55/af02708f230eb77084a299d7b08175cff006dea4f2721074b92cdb0296c0/ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562", size = 7634, upload-time = "2022-01-26T14:38:48.677Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "25.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "2.22"
|
version = "2.22"
|
||||||
@@ -111,6 +147,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pypresence"
|
name = "pypresence"
|
||||||
version = "4.6.1"
|
version = "4.6.1"
|
||||||
@@ -122,11 +167,12 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytaiko"
|
name = "pytaiko"
|
||||||
version = "1.0"
|
version = "1.1"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "av" },
|
{ name = "av" },
|
||||||
{ name = "pypresence" },
|
{ name = "pypresence" },
|
||||||
|
{ name = "pytest" },
|
||||||
{ name = "raylib-sdl" },
|
{ name = "raylib-sdl" },
|
||||||
{ name = "tomlkit" },
|
{ name = "tomlkit" },
|
||||||
]
|
]
|
||||||
@@ -140,6 +186,7 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "av", specifier = ">=16.0.1" },
|
{ name = "av", specifier = ">=16.0.1" },
|
||||||
{ name = "pypresence", specifier = ">=4.6.1" },
|
{ name = "pypresence", specifier = ">=4.6.1" },
|
||||||
|
{ name = "pytest", specifier = ">=9.0.2" },
|
||||||
{ name = "raylib-sdl", specifier = ">=5.5.0.2" },
|
{ name = "raylib-sdl", specifier = ">=5.5.0.2" },
|
||||||
{ name = "tomlkit", specifier = ">=0.13.3" },
|
{ name = "tomlkit", specifier = ">=0.13.3" },
|
||||||
]
|
]
|
||||||
@@ -147,6 +194,22 @@ requires-dist = [
|
|||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [{ name = "nuitka", specifier = ">=2.8.4" }]
|
dev = [{ name = "nuitka", specifier = ">=2.8.4" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raylib-sdl"
|
name = "raylib-sdl"
|
||||||
version = "5.5.0.3"
|
version = "5.5.0.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user