chore: checkpoint app work before firmware sync
This commit is contained in:
@@ -30,6 +30,15 @@ class LiveCaptionsContractTests(unittest.TestCase):
|
||||
self.assertIn("const char* stop", source)
|
||||
self.assertNotIn("i2s_controller_", source)
|
||||
|
||||
def test_ui_autostarts_and_keeps_only_the_latest_thirty_words(self):
|
||||
source = (APP / "main" / "Source" / "main.c").read_text()
|
||||
self.assertIn("#define DISPLAY_WORD_LIMIT 50", source)
|
||||
self.assertIn("copy_recent_words", source)
|
||||
self.assertIn("tt_lvgl_toolbar_create_for_app", source)
|
||||
self.assertNotIn("lv_btn_create", source)
|
||||
self.assertNotIn("start_button", source)
|
||||
self.assertIn("xTaskCreate(worker_task", source)
|
||||
|
||||
def test_connection_failure_stays_failed_until_user_starts_again(self):
|
||||
source = (APP / "main" / "Source" / "main.c").read_text()
|
||||
self.assertIn("CAPTION_FAILED", source)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MP3_SOURCE = Path(__file__).parents[1] / "Apps" / "Mp3Player" / "main" / "Source" / "main.c"
|
||||
|
||||
|
||||
def test_mp3_player_uses_the_firmware_audio_stream_device_name():
|
||||
source = MP3_SOURCE.read_text(encoding="utf-8")
|
||||
|
||||
assert 'device_find_by_name("audio-stream0")' in source
|
||||
assert 'device_find_by_name("audio-stream")' not in source
|
||||
assert "device_get_first_by_type" not in source
|
||||
assert "device_find_first_by_type" not in source
|
||||
|
||||
|
||||
def test_mp3_player_restores_a_visible_persisted_volume_control_above_the_six_button_row():
|
||||
source = MP3_SOURCE.read_text(encoding="utf-8")
|
||||
|
||||
assert "lv_obj_t* card = lv_obj_create(parent);" not in source
|
||||
assert "lv_label_set_text(icon, LV_SYMBOL_AUDIO);" not in source
|
||||
assert "lv_obj_t* vol_box = lv_obj_create(parent);" in source
|
||||
assert "g_ctx.slider_volume = lv_slider_create(vol_box);" in source
|
||||
assert "lv_slider_set_value(g_ctx.slider_volume, g_ctx.volume, LV_ANIM_OFF);" in source
|
||||
assert "on_volume_slider_changed" in source
|
||||
assert "save_volume(ctx);" in source
|
||||
assert "lv_obj_set_size(bottom_box, lv_pct(100), 52);" in source
|
||||
assert "lv_obj_t* btn_close = lv_btn_create(ctrl_box);" in source
|
||||
assert "lv_obj_t* btn_hist = lv_btn_create(ctrl_box);" in source
|
||||
assert "lv_label_set_text(lbl_close, LV_SYMBOL_CLOSE);" in source
|
||||
assert "lv_label_set_text(lbl_hist_btn, LV_SYMBOL_DIRECTORY);" in source
|
||||
assert "lv_label_set_text(lbl_back, \"-15s\");" in source
|
||||
assert "lv_label_set_text(lbl_fwd, \"+15s\");" in source
|
||||
|
||||
|
||||
def test_mp3_player_folder_button_returns_to_the_files_list():
|
||||
source = MP3_SOURCE.read_text(encoding="utf-8")
|
||||
|
||||
library_callback = source.split("static void on_library_click", 1)[1].split("/* ─── App Lifecycle", 1)[0]
|
||||
assert "tt_app_stop();" not in library_callback
|
||||
assert "show_history_screen(&g_ctx);" in library_callback
|
||||
assert "load_first_sd_mp3" not in library_callback
|
||||
|
||||
|
||||
def test_mp3_player_history_view_and_fifteen_second_seek_are_present():
|
||||
source = MP3_SOURCE.read_text(encoding="utf-8")
|
||||
|
||||
assert "#define SEEK_SECONDS 15" in source
|
||||
assert 'tt_app_get_user_data_child_path(app, "play_history.txt"' in source
|
||||
assert "static void show_history_screen" in source
|
||||
assert "static void on_history_selected" in source
|
||||
assert "char label[640];" in source
|
||||
assert "request_seek(ctx, -SEEK_SECONDS);" in source
|
||||
assert "request_seek(ctx, SEEK_SECONDS);" in source
|
||||
|
||||
|
||||
def test_mp3_player_persists_and_resumes_short_tracks_too():
|
||||
source = MP3_SOURCE.read_text(encoding="utf-8")
|
||||
history_selector = source.split("static void on_history_selected", 1)[1].split("static void refresh_history_list", 1)[0]
|
||||
|
||||
assert "Skip history: audio too short" not in source
|
||||
assert "Resuming last 10min+ play" not in source
|
||||
assert "Resuming last play %s at %d sec from history" in source
|
||||
assert "if (he->total_sec >= 600)" not in history_selector
|
||||
assert "resume_pos = he->pos_sec;" in history_selector
|
||||
|
||||
|
||||
def test_mp3_player_close_waits_for_playback_before_stopping_the_app():
|
||||
source = MP3_SOURCE.read_text(encoding="utf-8")
|
||||
|
||||
close_callback = source.split("static void on_close_click", 1)[1].split("static void on_library_click", 1)[0]
|
||||
assert "wait_for_playback_task_to_exit(&g_ctx);" in close_callback
|
||||
assert close_callback.index("wait_for_playback_task_to_exit(&g_ctx);") < close_callback.index("tt_app_stop();")
|
||||
assert "tt_lvgl_unlock();" in source
|
||||
assert "tt_lvgl_lock(portMAX_DELAY);" in source
|
||||
|
||||
|
||||
def test_mp3_player_has_a_consumed_dev_autoclose_hook_for_device_tests():
|
||||
source = MP3_SOURCE.read_text(encoding="utf-8")
|
||||
|
||||
assert '"mp3player_dev_autoclose_ms"' in source
|
||||
assert "on_close_click(NULL);" in source
|
||||
assert "unlink(marker_path);" in source
|
||||
@@ -0,0 +1,69 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
RUNNER_PATH = Path(__file__).parents[1] / "scripts" / "mp3_player_device_runner.py"
|
||||
|
||||
|
||||
def load_runner():
|
||||
spec = importlib.util.spec_from_file_location("mp3_player_device_runner", RUNNER_PATH)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_resume_trace_requires_playback_persistence_and_same_path_resume():
|
||||
runner = load_runner()
|
||||
fixture = "/sdcard/download/test.mp3"
|
||||
trace = "\n".join(
|
||||
[
|
||||
"I Mp3Player: Starting MP3 playback: /sdcard/download/test.mp3 (size: 480000 bytes) resume=0",
|
||||
"I Mp3Player: Audio stream opened: 16000 Hz, 1 channels",
|
||||
"I Mp3Player: History saved on hide: /sdcard/download/test.mp3 pos 9 total 30",
|
||||
"I Mp3Player: Resuming last play /sdcard/download/test.mp3 at 9 sec from history",
|
||||
"I Mp3Player: Starting MP3 playback: /sdcard/download/test.mp3 (size: 480000 bytes) resume=9",
|
||||
]
|
||||
)
|
||||
|
||||
result = runner.evaluate_resume_trace(trace, fixture, minimum_position=6)
|
||||
|
||||
assert result == {"persisted_position": 9, "resumed_position": 9}
|
||||
|
||||
|
||||
def test_resume_trace_rejects_a_different_file_or_no_playback_evidence():
|
||||
runner = load_runner()
|
||||
fixture = "/sdcard/download/test.mp3"
|
||||
|
||||
try:
|
||||
runner.evaluate_resume_trace(
|
||||
"I Mp3Player: Starting MP3 playback: /sdcard/other.mp3 (size: 1 bytes) resume=0",
|
||||
fixture,
|
||||
minimum_position=6,
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
assert "playback" in str(exc).lower()
|
||||
else:
|
||||
raise AssertionError("missing playback evidence must fail")
|
||||
|
||||
|
||||
def test_resume_trace_rejects_forced_playback_task_termination():
|
||||
runner = load_runner()
|
||||
fixture = "/sdcard/download/test.mp3"
|
||||
trace = "\n".join(
|
||||
[
|
||||
f"I Mp3Player: Starting MP3 playback: {fixture} (size: 480000 bytes) resume=0",
|
||||
"I Mp3Player: Audio stream opened: 16000 Hz, 1 channels",
|
||||
f"I Mp3Player: History saved on hide: {fixture} pos 9 total 180",
|
||||
"W Mp3Player: Playback task stuck, force deleting",
|
||||
f"I Mp3Player: Resuming last play {fixture} at 9 sec from history",
|
||||
f"I Mp3Player: Starting MP3 playback: {fixture} (size: 480000 bytes) resume=9",
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
runner.evaluate_resume_trace(trace, fixture, minimum_position=6)
|
||||
except RuntimeError as exc:
|
||||
assert "forced" in str(exc).lower()
|
||||
else:
|
||||
raise AssertionError("forced task termination must fail the device test")
|
||||
Reference in New Issue
Block a user