mirror of
https://github.com/Yonokid/PyTaiko.git
synced 2026-02-04 03:30:13 +01:00
tests (alleged)
This commit is contained in:
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()
|
||||
Reference in New Issue
Block a user