Skip to content

Commit a628bfb

Browse files
test: add failing tests for state persistence bugs
Add comprehensive test suite to reproduce state persistence issues: - Tests for hot reload losing state - Tests for concurrent update race conditions - Tests for storage initialization order problems - BDD feature file with Gherkin scenarios These tests are expected to fail until we implement proper server-side state persistence and fix the architectural issues. Related to #13
1 parent 15cdb00 commit a628bfb

File tree

2 files changed

+283
-0
lines changed

2 files changed

+283
-0
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
Feature: Persistent Game State
2+
As a bingo game player
3+
I want my game state to persist across app restarts
4+
So that I don't lose progress if the app needs to restart
5+
6+
Background:
7+
Given I have a bingo game in progress
8+
And I have clicked tiles at positions "(0,1)", "(2,3)", and "(4,4)"
9+
And the game header shows "3 tiles clicked"
10+
11+
Scenario: State persists through graceful restart
12+
When the app restarts gracefully
13+
Then the clicked tiles should remain at "(0,1)", "(2,3)", and "(4,4)"
14+
And the header should still show "3 tiles clicked"
15+
And the board should show the same phrases
16+
17+
Scenario: State persists through unexpected restart
18+
When the app crashes and restarts
19+
Then the clicked tiles should remain at "(0,1)", "(2,3)", and "(4,4)"
20+
And the header should still show "3 tiles clicked"
21+
And the board should show the same phrases
22+
23+
Scenario: State persists when code changes trigger reload
24+
When I modify a source file
25+
And NiceGUI triggers a hot reload
26+
Then the game state should be preserved
27+
And all clicked tiles should remain clicked
28+
29+
Scenario: Multiple users maintain separate views
30+
Given User A is on the main page
31+
And User B is on the stream page
32+
When the app restarts
33+
Then User A should see the interactive board with saved state
34+
And User B should see the read-only board with saved state
35+
And both users should see the same clicked tiles
36+
37+
Scenario: Concurrent updates don't cause data loss
38+
Given two users are playing simultaneously
39+
When User A clicks tile "(1,1)"
40+
And User B clicks tile "(2,2)" at the same time
41+
Then both tiles should be marked as clicked
42+
And the state should show 5 total clicked tiles
43+
44+
Scenario: State recovery from corruption
45+
Given the stored state becomes corrupted
46+
When the app tries to load the corrupted state
47+
Then it should log an error
48+
And it should start with a fresh game
49+
And it should not crash
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
"""
2+
Tests that reproduce state persistence bugs during app restarts.
3+
These tests are expected to FAIL until we fix the underlying issues.
4+
"""
5+
6+
import pytest
7+
import asyncio
8+
from unittest.mock import Mock, patch, MagicMock
9+
from nicegui import app, ui
10+
11+
from src.core.game_logic import (
12+
save_state_to_storage,
13+
load_state_from_storage,
14+
board, clicked_tiles, is_game_closed,
15+
board_iteration, bingo_patterns, today_seed,
16+
toggle_tile, generate_board
17+
)
18+
from src.utils.file_operations import read_phrases_file
19+
20+
21+
class TestStatePersistenceBugs:
22+
"""Tests that demonstrate current bugs in state persistence."""
23+
24+
def setup_method(self):
25+
"""Set up test environment."""
26+
# Reset global state
27+
global board, clicked_tiles, is_game_closed, board_iteration
28+
board.clear()
29+
clicked_tiles.clear()
30+
is_game_closed = False
31+
board_iteration = 1
32+
33+
# Mock app.storage.general
34+
if not hasattr(app, 'storage'):
35+
app.storage = Mock()
36+
app.storage.general = {}
37+
38+
def test_state_not_restored_after_hot_reload(self):
39+
"""Test that state is lost when NiceGUI triggers a hot reload."""
40+
# Arrange: Set up game state
41+
phrases = read_phrases_file()
42+
generate_board(1, phrases)
43+
toggle_tile(0, 0)
44+
toggle_tile(1, 1)
45+
assert len(clicked_tiles) == 2
46+
47+
# Save state
48+
assert save_state_to_storage() is True
49+
assert 'game_state' in app.storage.general
50+
51+
# Simulate hot reload by clearing globals (what actually happens)
52+
board.clear()
53+
clicked_tiles.clear()
54+
55+
# Act: Try to restore state
56+
restored = load_state_from_storage()
57+
58+
# Assert: This currently FAILS because globals aren't properly restored
59+
assert restored is True
60+
assert len(clicked_tiles) == 2 # This will fail!
61+
assert (0, 0) in clicked_tiles
62+
assert (1, 1) in clicked_tiles
63+
64+
def test_storage_persistence_across_app_restart(self):
65+
"""Test that storage actually persists when app fully restarts."""
66+
# Arrange: Save some state
67+
phrases = read_phrases_file()
68+
generate_board(1, phrases)
69+
toggle_tile(2, 2)
70+
save_state_to_storage()
71+
72+
# Simulate app restart by creating new storage instance
73+
old_storage = app.storage.general.copy()
74+
app.storage.general = {} # This simulates what happens on restart
75+
76+
# Act: Try to load state
77+
restored = load_state_from_storage()
78+
79+
# Assert: This FAILS because storage is cleared on restart
80+
assert restored is True # This will fail!
81+
assert len(clicked_tiles) == 1
82+
83+
@pytest.mark.asyncio
84+
async def test_concurrent_state_updates_cause_data_loss(self):
85+
"""Test that concurrent state updates don't cause data loss."""
86+
# Arrange: Initial state
87+
phrases = read_phrases_file()
88+
generate_board(1, phrases)
89+
90+
# Simulate concurrent updates from multiple users
91+
async def user1_clicks():
92+
toggle_tile(0, 0)
93+
await asyncio.sleep(0.01) # Simulate network delay
94+
save_state_to_storage()
95+
96+
async def user2_clicks():
97+
toggle_tile(1, 1)
98+
await asyncio.sleep(0.01) # Simulate network delay
99+
save_state_to_storage()
100+
101+
# Act: Run concurrent updates
102+
await asyncio.gather(user1_clicks(), user2_clicks())
103+
104+
# Reload state
105+
clicked_tiles.clear()
106+
load_state_from_storage()
107+
108+
# Assert: Both clicks should be saved (this may fail due to race condition)
109+
assert len(clicked_tiles) == 2
110+
assert (0, 0) in clicked_tiles
111+
assert (1, 1) in clicked_tiles
112+
113+
def test_state_corruption_on_partial_write(self):
114+
"""Test that partial writes don't corrupt state."""
115+
# Arrange: Set up state
116+
phrases = read_phrases_file()
117+
generate_board(1, phrases)
118+
toggle_tile(0, 0)
119+
save_state_to_storage()
120+
121+
# Simulate partial write by corrupting storage
122+
app.storage.general['game_state']['clicked_tiles'] = "corrupted"
123+
124+
# Act: Try to load corrupted state
125+
clicked_tiles.clear()
126+
restored = load_state_from_storage()
127+
128+
# Assert: Should handle corruption gracefully (currently doesn't)
129+
assert restored is False # This might fail if error handling is poor
130+
assert len(clicked_tiles) == 0 # Should be empty after failed load
131+
132+
def test_init_app_called_multiple_times(self):
133+
"""Test that calling init_app multiple times doesn't lose state."""
134+
from app import init_app
135+
136+
# Arrange: Set up initial state
137+
phrases = read_phrases_file()
138+
generate_board(1, phrases)
139+
toggle_tile(3, 3)
140+
save_state_to_storage()
141+
initial_tiles = clicked_tiles.copy()
142+
143+
# Act: Call init_app again (simulates hot reload scenario)
144+
with patch('app.init_routes'):
145+
init_app()
146+
147+
# Assert: State should be preserved
148+
assert clicked_tiles == initial_tiles # This may fail!
149+
150+
151+
class TestBDDStyleStatePersistence:
152+
"""BDD-style tests for state persistence scenarios."""
153+
154+
def setup_method(self):
155+
"""Given I have a bingo game in progress."""
156+
# Reset and initialize
157+
board.clear()
158+
clicked_tiles.clear()
159+
is_game_closed = False
160+
161+
if not hasattr(app, 'storage'):
162+
app.storage = Mock()
163+
app.storage.general = {}
164+
165+
# Set up game
166+
phrases = read_phrases_file()
167+
generate_board(1, phrases)
168+
169+
def test_scenario_graceful_restart(self):
170+
"""
171+
Scenario: State persists through graceful restart
172+
"""
173+
# Given I have clicked tiles at positions (0,1), (2,3), and (4,4)
174+
positions = [(0, 1), (2, 3), (4, 4)]
175+
for row, col in positions:
176+
toggle_tile(row, col)
177+
178+
# When the app restarts gracefully
179+
save_state_to_storage()
180+
181+
# Simulate restart
182+
clicked_tiles.clear()
183+
board.clear()
184+
185+
# Then the state should be restored
186+
assert load_state_from_storage() is True
187+
188+
# And the clicked tiles should remain
189+
for pos in positions:
190+
assert pos in clicked_tiles
191+
192+
# And the board should show the same phrases
193+
assert len(board) == 5
194+
assert len(board[0]) == 5
195+
196+
def test_scenario_code_change_reload(self):
197+
"""
198+
Scenario: State persists when code changes trigger reload
199+
"""
200+
# Given I have a game in progress
201+
toggle_tile(1, 1)
202+
toggle_tile(2, 2)
203+
original_tiles = clicked_tiles.copy()
204+
205+
# When I modify a source file and NiceGUI triggers a hot reload
206+
save_state_to_storage()
207+
208+
# Simulate what happens during hot reload
209+
import importlib
210+
import src.core.game_logic
211+
212+
# Clear module state (simulates reload)
213+
clicked_tiles.clear()
214+
board.clear()
215+
216+
# Reload module
217+
importlib.reload(src.core.game_logic)
218+
219+
# Then the game state should be preserved
220+
load_state_from_storage()
221+
assert clicked_tiles == original_tiles # This likely fails!
222+
223+
224+
def test_nicegui_storage_behavior():
225+
"""Test to understand actual NiceGUI storage behavior."""
226+
# This test documents the actual behavior we need to work with
227+
228+
# 1. Storage is only available after ui.run() is called
229+
# 2. Storage uses browser localStorage by default
230+
# 3. Server restarts clear server-side state
231+
# 4. Hot reloads may or may not preserve storage
232+
233+
# We need to architect around these constraints
234+
pass

0 commit comments

Comments
 (0)