"Version X is available" in the top bar is now a button that opens an Update popup: - Update and restart gets the download link from the server (the redirect is not followed, as the Gitea link needs no token), downloads the new game next to the running one with a progress bar, checks it (complete, and an MZ or ELF file), swaps it in and restarts. The quiz is saved first and continues. On Windows the running .exe is renamed to .old, put back if the swap fails, and deleted at the next start. - Download in browser opens the .exe, the Linux package, or the release page on other platforms. - From the editor, an unwritable folder or another platform, only the browser download is offered, with the reason. The message now fits between the tabs and the buttons in every theme; in Nord and Christmas it ran into Sync. The macOS export preset is removed, and the unused download_client_* functions are replaced by fetch_client_download_url. Closes #13 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
216 lines
7.6 KiB
GDScript
216 lines
7.6 KiB
GDScript
extends GdUnitTestSuite
|
|
## Updating the game in place: which file to get, checking it, swapping it in, and
|
|
## cleaning up after.
|
|
|
|
const WINDOWS_GAME: PackedByteArray = [0x4D, 0x5A, 0x90, 0x00, 0x03]
|
|
const LINUX_GAME: PackedByteArray = [0x7F, 0x45, 0x4C, 0x46, 0x02]
|
|
|
|
var _dir: String
|
|
var _updater: Updater
|
|
|
|
|
|
func before_test() -> void:
|
|
_dir = create_temp_dir("updater_%d" % randi())
|
|
_updater = auto_free(Updater.new())
|
|
_updater.in_editor = false
|
|
_updater.os_name = "Windows"
|
|
_updater.executable_path = _dir.path_join("MusicPlayer.exe")
|
|
|
|
|
|
func _write(path: String, bytes: PackedByteArray) -> String:
|
|
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
|
file.store_buffer(bytes)
|
|
file.close()
|
|
return path
|
|
|
|
|
|
func _read(path: String) -> PackedByteArray:
|
|
return FileAccess.get_file_as_bytes(path)
|
|
|
|
|
|
@warning_ignore("unused_parameter")
|
|
func test_asset_for(os_name: String, in_place: bool, asset: String, test_parameters: Array = [
|
|
["Windows", true, "windows"],
|
|
["Windows", false, "windows"],
|
|
["Linux", true, "linux"],
|
|
["Linux", false, "linux-package"],
|
|
["macOS", true, ""],
|
|
["macOS", false, ""],
|
|
]) -> void:
|
|
assert_str(Updater.asset_for(os_name, in_place)).is_equal(asset)
|
|
|
|
|
|
@warning_ignore("unused_parameter")
|
|
func test_check_file(bytes: PackedByteArray, os_name: String, ok: bool,
|
|
test_parameters: Array = [
|
|
[WINDOWS_GAME, "Windows", true],
|
|
[LINUX_GAME, "Linux", true],
|
|
[LINUX_GAME, "Windows", false],
|
|
[WINDOWS_GAME, "Linux", false],
|
|
["<html>Not Found</html>".to_utf8_buffer(), "Windows", false],
|
|
[PackedByteArray(), "Linux", false],
|
|
]) -> void:
|
|
var path: String = _write(_dir.path_join("game"), bytes)
|
|
|
|
assert_bool(Updater.check_file(path, os_name)).is_equal(ok)
|
|
|
|
|
|
func test_missing_file_is_not_the_game() -> void:
|
|
assert_bool(Updater.check_file(_dir.path_join("nothing"), "Windows")).is_false()
|
|
|
|
|
|
func test_windows_moves_the_running_game_aside() -> void:
|
|
var exe: String = _write(_dir.path_join("MusicPlayer.exe"), "old".to_utf8_buffer())
|
|
var update: String = _write(exe + Updater.UPDATE_SUFFIX, "new".to_utf8_buffer())
|
|
|
|
assert_int(Updater.install(update, exe, "Windows")).is_equal(OK)
|
|
|
|
assert_str(_read(exe).get_string_from_utf8()).is_equal("new")
|
|
assert_str(_read(exe + Updater.OLD_SUFFIX).get_string_from_utf8()).is_equal("old")
|
|
assert_bool(FileAccess.file_exists(update)).is_false()
|
|
|
|
|
|
func test_windows_replaces_an_old_version_left_from_before() -> void:
|
|
var exe: String = _write(_dir.path_join("MusicPlayer.exe"), "old".to_utf8_buffer())
|
|
_write(exe + Updater.OLD_SUFFIX, "older".to_utf8_buffer())
|
|
var update: String = _write(exe + Updater.UPDATE_SUFFIX, "new".to_utf8_buffer())
|
|
|
|
assert_int(Updater.install(update, exe, "Windows")).is_equal(OK)
|
|
|
|
assert_str(_read(exe + Updater.OLD_SUFFIX).get_string_from_utf8()).is_equal("old")
|
|
|
|
|
|
func test_windows_puts_the_game_back_when_the_new_one_cannot_be_moved_in() -> void:
|
|
var exe: String = _write(_dir.path_join("MusicPlayer.exe"), "old".to_utf8_buffer())
|
|
|
|
var error: Error = Updater.install(exe + Updater.UPDATE_SUFFIX, exe, "Windows")
|
|
|
|
assert_int(error).is_not_equal(OK)
|
|
assert_str(_read(exe).get_string_from_utf8()).is_equal("old")
|
|
assert_bool(FileAccess.file_exists(exe + Updater.OLD_SUFFIX)).is_false()
|
|
|
|
|
|
func test_linux_replaces_the_game_and_makes_it_runnable() -> void:
|
|
var exe: String = _write(_dir.path_join("MusicPlayer.x86_64"), "old".to_utf8_buffer())
|
|
var update: String = _write(exe + Updater.UPDATE_SUFFIX, "new".to_utf8_buffer())
|
|
|
|
assert_int(Updater.install(update, exe, "Linux")).is_equal(OK)
|
|
|
|
assert_str(_read(exe).get_string_from_utf8()).is_equal("new")
|
|
assert_bool(FileAccess.file_exists(update)).is_false()
|
|
assert_int(FileAccess.get_unix_permissions(exe) & FileAccess.UNIX_EXECUTE_OWNER) \
|
|
.is_not_equal(0)
|
|
|
|
|
|
func test_leftovers_are_deleted_at_start() -> void:
|
|
var exe: String = _write(_dir.path_join("MusicPlayer.exe"), "new".to_utf8_buffer())
|
|
_write(exe + Updater.OLD_SUFFIX, "old".to_utf8_buffer())
|
|
_write(exe + Updater.UPDATE_SUFFIX, "half".to_utf8_buffer())
|
|
|
|
Updater.remove_old_files(exe)
|
|
|
|
assert_bool(FileAccess.file_exists(exe)).is_true()
|
|
assert_bool(FileAccess.file_exists(exe + Updater.OLD_SUFFIX)).is_false()
|
|
assert_bool(FileAccess.file_exists(exe + Updater.UPDATE_SUFFIX)).is_false()
|
|
|
|
|
|
func test_can_update_in_a_writable_folder() -> void:
|
|
assert_str(_updater.can_update_in_place()).is_empty()
|
|
assert_array(DirAccess.get_files_at(_dir)).is_empty()
|
|
|
|
|
|
func test_not_from_the_editor() -> void:
|
|
_updater.in_editor = true
|
|
|
|
assert_str(_updater.can_update_in_place()).contains("editor")
|
|
|
|
|
|
func test_not_on_other_platforms() -> void:
|
|
_updater.os_name = "macOS"
|
|
|
|
assert_str(_updater.can_update_in_place()).contains("Windows and Linux")
|
|
|
|
|
|
func test_not_when_the_folder_cannot_be_written_to() -> void:
|
|
_updater.executable_path = _dir.path_join("missing/MusicPlayer.exe")
|
|
|
|
assert_str(_updater.can_update_in_place()).contains("can't be written to")
|
|
|
|
|
|
func test_restart_starts_the_game_and_quits() -> void:
|
|
var started: Array[String] = []
|
|
var quits: Array[bool] = []
|
|
_updater.start_process = func(path: String, _arguments: PackedStringArray) -> int:
|
|
started.append(path)
|
|
return 1
|
|
_updater.quit = func() -> void: quits.append(true)
|
|
|
|
_updater.restart()
|
|
|
|
assert_array(started).contains_exactly([_updater.executable_path])
|
|
assert_array(quits).has_size(1)
|
|
|
|
|
|
# Answers one HTTP request on a local port with [param status] and [param body], and
|
|
# downloads it with the updater. Returns [done paths, failure reasons].
|
|
func _download_from_local_server(status: String, body: PackedByteArray) -> Array:
|
|
var server: TCPServer = TCPServer.new()
|
|
assert_int(server.listen(0, "127.0.0.1")).is_equal(OK)
|
|
add_child(_updater)
|
|
var done: Array[String] = []
|
|
var failed: Array[String] = []
|
|
var progress: Array[int] = []
|
|
_updater.download("http://127.0.0.1:%d/MusicPlayer.exe" % server.get_local_port(),
|
|
func(bytes: int, _total: int) -> void: progress.append(bytes),
|
|
func(path: String) -> void: done.append(path),
|
|
func(reason: String) -> void: failed.append(reason))
|
|
var connection: StreamPeerTCP = null
|
|
for i: int in 100:
|
|
await await_millis(20)
|
|
if connection == null && server.is_connection_available():
|
|
connection = server.take_connection()
|
|
if connection != null && connection.get_available_bytes() > 0:
|
|
connection.get_data(connection.get_available_bytes())
|
|
var head: String = "HTTP/1.1 %s\r\nContent-Length: %d\r\nConnection: close\r\n\r\n" \
|
|
% [status, body.size()]
|
|
connection.put_data(head.to_ascii_buffer() + body)
|
|
break
|
|
for i: int in 100:
|
|
await await_millis(20)
|
|
if !done.is_empty() || !failed.is_empty():
|
|
break
|
|
if connection != null:
|
|
connection.disconnect_from_host()
|
|
server.stop()
|
|
remove_child(_updater)
|
|
return [done, failed, progress]
|
|
|
|
|
|
func test_download_keeps_a_checked_game() -> void:
|
|
var game: PackedByteArray = WINDOWS_GAME.duplicate()
|
|
game.resize(4096)
|
|
|
|
var result: Array = await _download_from_local_server("200 OK", game)
|
|
|
|
assert_array(result[1]).is_empty()
|
|
assert_array(result[0]).contains_exactly([_updater.update_path()])
|
|
assert_array(_read(_updater.update_path())).is_equal(game)
|
|
|
|
|
|
func test_download_of_an_error_page_is_deleted() -> void:
|
|
var result: Array = await _download_from_local_server("404 Not Found",
|
|
"Not Found".to_utf8_buffer())
|
|
|
|
assert_array(result[0]).is_empty()
|
|
assert_str(result[1][0]).contains("404")
|
|
assert_bool(FileAccess.file_exists(_updater.update_path())).is_false()
|
|
|
|
|
|
func test_download_that_is_not_the_game_is_deleted() -> void:
|
|
var result: Array = await _download_from_local_server("200 OK",
|
|
"<html>Sign in</html>".to_utf8_buffer())
|
|
|
|
assert_array(result[0]).is_empty()
|
|
assert_str(result[1][0]).contains("not the game")
|
|
assert_bool(FileAccess.file_exists(_updater.update_path())).is_false()
|