Settings has a Music folder row. A folder with subfolders is played as soundtracks: Next picks a random subfolder and a random song in it, the subfolder is the answer's soundtrack and the file name without its type the song. The inspiration list and Search show the subfolders. A folder with songs and no subfolders is a prepared quiz, played once in order (natural sort) or at random, showing only the song title. It has no winning score: after the last song Next becomes Show winner, and the players with the most points win, ties included. While a folder is used nothing but the characters is asked from the server: Sync, QR, Statistics, the update check and play counting are off. Characters fall back to the cached pictures when the server can't be reached. mp3, ogg and wav play; other files and broken ones are skipped. The old local playback code, the hidden Open button and the hidden Local toggle are removed. Next now waits for the song to start before preparing the next one.
215 lines
7.0 KiB
GDScript
215 lines
7.0 KiB
GDScript
extends GdUnitTestSuite
|
|
## Character pictures cached on disk: downloaded once, then only again when they changed
|
|
## on the server, or when they are old and the server can't tell.
|
|
|
|
var _server: FakeHttpTransport
|
|
var _config_dir_before: String
|
|
var _png: PackedByteArray
|
|
|
|
|
|
func before_test() -> void:
|
|
_server = FakeHttpTransport.new().install()
|
|
_config_dir_before = OS.get_environment(Settings.CONFIG_DIR_ENV)
|
|
OS.set_environment(Settings.CONFIG_DIR_ENV, create_temp_dir("character_cache"))
|
|
if DirAccess.dir_exists_absolute(CharacterCache.folder()):
|
|
for file_name: String in DirAccess.get_files_at(CharacterCache.folder()):
|
|
DirAccess.remove_absolute(CharacterCache.folder().path_join(file_name))
|
|
var image: Image = Image.create(4, 4, false, Image.FORMAT_RGB8)
|
|
image.fill(Color.ORANGE)
|
|
_png = image.save_png_to_buffer()
|
|
|
|
|
|
func after_test() -> void:
|
|
OS.set_environment(Settings.CONFIG_DIR_ENV, _config_dir_before)
|
|
_server.uninstall()
|
|
|
|
|
|
func _serve(characters: Dictionary) -> void:
|
|
var details: Array = []
|
|
for character_name: String in characters:
|
|
details.append({"name": character_name, "modified": characters[character_name]})
|
|
_server.respond_bytes(_image_path(character_name), _png)
|
|
_server.respond_json("/character/details", details)
|
|
|
|
|
|
# Serves the names only, like a server older than /character/details
|
|
func _serve_names_only(names: Array[String]) -> void:
|
|
_server.respond_json("/character/list", names)
|
|
for character_name: String in names:
|
|
_server.respond_bytes(_image_path(character_name), _png)
|
|
|
|
|
|
func _image_path(character_name: String) -> String:
|
|
return "/character?name=" + character_name.uri_encode()
|
|
|
|
|
|
# Loads the list and every picture with a new cache, like starting the game
|
|
func _load_all() -> Array[Image]:
|
|
var cache: CharacterCache = CharacterCache.new()
|
|
var images: Array[Image] = []
|
|
cache.load_list(func(names: Array[String]) -> void:
|
|
for character_name: String in names:
|
|
cache.fetch_image(character_name, func(image: Image) -> void: images.append(image)))
|
|
return images
|
|
|
|
|
|
func _index() -> Dictionary:
|
|
return JSON.parse_string(FileAccess.get_file_as_string(
|
|
CharacterCache.folder().path_join(CharacterCache.INDEX_FILE_NAME)))
|
|
|
|
|
|
func _set_index(index: Dictionary) -> void:
|
|
FileAccess.open(CharacterCache.folder().path_join(CharacterCache.INDEX_FILE_NAME),
|
|
FileAccess.WRITE).store_string(JSON.stringify(index))
|
|
|
|
|
|
func test_first_load_downloads_and_keeps_every_picture() -> void:
|
|
_serve({"Crono.png": 100, "Marle.png": 200})
|
|
|
|
var images: Array[Image] = _load_all()
|
|
|
|
assert_int(images.size()).is_equal(2)
|
|
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
|
assert_bool(FileAccess.file_exists(CharacterCache.folder().path_join("Crono.png"))).is_true()
|
|
assert_int(int(_index()["Marle.png"]["modified"])).is_equal(200)
|
|
|
|
|
|
func test_unchanged_pictures_come_from_disk() -> void:
|
|
_serve({"Crono.png": 100, "Marle.png": 200})
|
|
_load_all()
|
|
|
|
var images: Array[Image] = _load_all()
|
|
|
|
assert_int(images.size()).is_equal(2)
|
|
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
|
assert_int(_server.count(_image_path("Marle.png"))).is_equal(1)
|
|
|
|
|
|
func test_only_changed_pictures_are_downloaded_again() -> void:
|
|
_serve({"Crono.png": 100, "Marle.png": 200})
|
|
_load_all()
|
|
|
|
_serve({"Crono.png": 100, "Marle.png": 300})
|
|
_load_all()
|
|
|
|
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
|
assert_int(_server.count(_image_path("Marle.png"))).is_equal(2)
|
|
assert_int(int(_index()["Marle.png"]["modified"])).is_equal(300)
|
|
|
|
|
|
func test_new_character_is_downloaded() -> void:
|
|
_serve({"Crono.png": 100})
|
|
_load_all()
|
|
|
|
_serve({"Crono.png": 100, "Lucca.png": 100})
|
|
var images: Array[Image] = _load_all()
|
|
|
|
assert_int(images.size()).is_equal(2)
|
|
assert_int(_server.count(_image_path("Lucca.png"))).is_equal(1)
|
|
|
|
|
|
func test_characters_removed_on_the_server_are_deleted() -> void:
|
|
_serve({"Crono.png": 100, "Magus.png": 100})
|
|
_load_all()
|
|
|
|
_serve({"Crono.png": 100})
|
|
_load_all()
|
|
|
|
assert_bool(FileAccess.file_exists(CharacterCache.folder().path_join("Magus.png"))) \
|
|
.is_false()
|
|
assert_bool(_index().has("Magus.png")).is_false()
|
|
|
|
|
|
func test_broken_cached_picture_is_downloaded_again() -> void:
|
|
_serve({"Crono.png": 100})
|
|
_load_all()
|
|
FileAccess.open(CharacterCache.folder().path_join("Crono.png"), FileAccess.WRITE) \
|
|
.store_string("not a picture")
|
|
|
|
var images: Array[Image] = _load_all()
|
|
|
|
assert_int(images.size()).is_equal(1)
|
|
assert_int(_server.count(_image_path("Crono.png"))).is_equal(2)
|
|
|
|
|
|
func test_older_server_lists_names_and_young_pictures_come_from_disk() -> void:
|
|
_serve_names_only(["Crono.png"])
|
|
_load_all()
|
|
|
|
var images: Array[Image] = _load_all()
|
|
|
|
assert_int(images.size()).is_equal(1)
|
|
assert_int(_server.count("/character/details")).is_equal(2)
|
|
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
|
assert_int(int(_index()["Crono.png"]["modified"])).is_equal(CharacterCache.UNKNOWN_TIME)
|
|
|
|
|
|
func test_older_server_pictures_are_downloaded_again_when_old() -> void:
|
|
_serve_names_only(["Crono.png", "Marle.png"])
|
|
_load_all()
|
|
var index: Dictionary = _index()
|
|
var too_old: int = int(Time.get_unix_time_from_system()) - CharacterCache.MAX_AGE_SECONDS - 1
|
|
index["Marle.png"]["downloaded"] = too_old
|
|
_set_index(index)
|
|
|
|
_load_all()
|
|
|
|
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
|
assert_int(_server.count(_image_path("Marle.png"))).is_equal(2)
|
|
|
|
|
|
func test_picture_cached_without_a_time_is_downloaded_once_the_server_has_times() -> void:
|
|
_serve_names_only(["Crono.png"])
|
|
_load_all()
|
|
|
|
_serve({"Crono.png": 100})
|
|
_load_all()
|
|
_load_all()
|
|
|
|
assert_int(_server.count(_image_path("Crono.png"))).is_equal(2)
|
|
|
|
|
|
func test_picture_that_cannot_be_read_is_not_cached() -> void:
|
|
_server.respond_json("/character/details", [{"name": "Crono.png", "modified": 100}])
|
|
_server.respond_bytes(_image_path("Crono.png"), "not a picture".to_utf8_buffer())
|
|
|
|
var images: Array[Image] = _load_all()
|
|
|
|
assert_array(images).is_empty()
|
|
assert_bool(FileAccess.file_exists(CharacterCache.folder().path_join("Crono.png"))).is_false()
|
|
|
|
|
|
func test_broken_index_starts_over() -> void:
|
|
_serve({"Crono.png": 100})
|
|
_load_all()
|
|
FileAccess.open(CharacterCache.folder().path_join(CharacterCache.INDEX_FILE_NAME),
|
|
FileAccess.WRITE).store_string("{broken")
|
|
|
|
var images: Array[Image] = _load_all()
|
|
|
|
assert_int(images.size()).is_equal(1)
|
|
assert_int(_server.count(_image_path("Crono.png"))).is_equal(2)
|
|
|
|
|
|
func test_cached_pictures_are_shown_when_the_server_cannot_be_reached() -> void:
|
|
_serve({"Crono.png": 100, "Marle.png": 200})
|
|
_load_all()
|
|
_server.respond_json("/character/details", {}, 503)
|
|
_server.respond_json("/character/list", {}, 503)
|
|
|
|
var images: Array[Image] = _load_all()
|
|
|
|
assert_int(images.size()).is_equal(2)
|
|
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
|
assert_bool(FileAccess.file_exists(CharacterCache.folder().path_join("Marle.png"))).is_true()
|
|
|
|
|
|
func test_nothing_is_shown_without_a_server_or_cached_pictures() -> void:
|
|
var cache: CharacterCache = CharacterCache.new()
|
|
var listed: Array = []
|
|
|
|
cache.load_list(func(names: Array[String]) -> void: listed.append(names))
|
|
|
|
assert_array(listed).has_size(1)
|
|
assert_array(listed[0]).is_empty()
|