-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
fix windows updater zip root path normalization #8019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b5cbcc6
7071819
e27094f
909feea
6de68a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| import ntpath | ||
| import posixpath | ||
| from dataclasses import dataclass, field | ||
| from pathlib import Path | ||
| from types import SimpleNamespace | ||
|
|
@@ -108,6 +110,27 @@ def stream(self, method: str, url: str): # noqa: ARG002 | |
| return _FakeFailingStreamResponse() | ||
|
|
||
|
|
||
| class _FakeZipArchive: | ||
| def __init__(self, names: list[str]): | ||
| self._names = names | ||
|
|
||
| def __enter__(self): | ||
| return self | ||
|
|
||
| def __exit__(self, exc_type, exc, tb) -> None: | ||
| return None | ||
|
|
||
| def namelist(self) -> list[str]: | ||
| return self._names | ||
|
|
||
| def extractall(self, target_dir: str) -> None: # noqa: ARG002 | ||
| return None | ||
|
|
||
|
|
||
| def _build_fake_archive_entries(archive_root: str) -> list[str]: | ||
| return [archive_root, posixpath.join(archive_root, ".dockerignore")] | ||
|
|
||
|
|
||
| def _build_fake_httpx_module(state: _FakeAsyncClientState) -> SimpleNamespace: | ||
| class _FakeAsyncClient: | ||
| def __init__(self, **kwargs): | ||
|
|
@@ -400,3 +423,161 @@ async def test_download_file_logs_url_and_target_path_on_failure( | |
|
|
||
| assert any(url in message for message in log_messages) | ||
| assert any(str(target_path) in message for message in log_messages) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "archive_root", | ||
| [ | ||
| "AstrBotDevs-AstrBot-39386ee/", | ||
| "AstrBotDevs-AstrBot-39386ee", | ||
| "owner-repo-branch/subdir/", | ||
| ".", | ||
| ], | ||
| ) | ||
| def test_repo_unzip_file_normalizes_windows_extended_length_paths( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| archive_root: str, | ||
| ) -> None: | ||
| import astrbot.core.zip_updator as zip_updator_module | ||
|
|
||
| target_dir = r"\\?\C:\Users\admin\AppData\Local\AstrBot\backend\app" | ||
| normalized_root = ntpath.normpath(archive_root) | ||
| expected_root = ( | ||
| target_dir | ||
| if normalized_root == "." | ||
| else ntpath.join(target_dir, normalized_root) | ||
| ) | ||
| expected_file = ntpath.join(expected_root, ".dockerignore") | ||
| captured: dict[str, object | None] = { | ||
| "listdir": None, | ||
| "move": None, | ||
| "cleanup": None, | ||
| "removed": None, | ||
| } | ||
|
|
||
| def fake_listdir(path: str) -> list[str]: | ||
| captured["listdir"] = path | ||
| return [".dockerignore"] | ||
|
|
||
| monkeypatch.setattr( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (testing): Assert that the zip file itself is removed, or avoid capturing it if you don’t use it In Suggested implementation: expected_root = ntpath.normpath(ntpath.join(target_dir, archive_root))
expected_file = ntpath.normpath(
ntpath.join(target_dir, archive_root, ".dockerignore")
)
# used to capture side effects like listdir path and removed zip archive
captured: dict[str, object] = {"removed": None}In
If you prefer not to assert on removal, instead delete the |
||
| zip_updator_module.os, "makedirs", lambda path, exist_ok=True: None | ||
| ) | ||
| monkeypatch.setattr(zip_updator_module.os.path, "join", ntpath.join) | ||
| monkeypatch.setattr(zip_updator_module.os.path, "normpath", ntpath.normpath) | ||
| monkeypatch.setattr(zip_updator_module.os.path, "isdir", lambda path: False) | ||
| monkeypatch.setattr(zip_updator_module.os.path, "exists", lambda path: False) | ||
| monkeypatch.setattr( | ||
| zip_updator_module.zipfile, | ||
| "ZipFile", | ||
| lambda path, mode: _FakeZipArchive(_build_fake_archive_entries(archive_root)), | ||
| ) | ||
| monkeypatch.setattr(zip_updator_module.logger, "debug", lambda message: None) | ||
| monkeypatch.setattr(zip_updator_module.logger, "warning", lambda message: None) | ||
| monkeypatch.setattr(zip_updator_module.os, "listdir", fake_listdir) | ||
| monkeypatch.setattr( | ||
| zip_updator_module.shutil, | ||
| "move", | ||
| lambda src, dst: captured.__setitem__("move", (src, dst)), | ||
| ) | ||
| monkeypatch.setattr( | ||
| zip_updator_module.shutil, | ||
| "rmtree", | ||
| lambda path, onerror=None: captured.__setitem__("cleanup", path), | ||
| ) | ||
| monkeypatch.setattr( | ||
| zip_updator_module.os, | ||
| "remove", | ||
| lambda path: captured.__setitem__("removed", path), | ||
| ) | ||
|
|
||
| RepoZipUpdator().unzip_file("temp.zip", target_dir) | ||
|
|
||
| assert captured["removed"] == "temp.zip" | ||
| if normalized_root == ".": | ||
| assert captured["listdir"] is None | ||
| assert captured["move"] is None | ||
| assert captured["cleanup"] is None | ||
| return | ||
|
|
||
| assert captured["listdir"] == expected_root | ||
| assert captured["move"] == (expected_file, target_dir) | ||
| assert captured["cleanup"] == expected_root | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "archive_root", | ||
| [ | ||
| "AstrBotDevs-demo-39386ee/", | ||
| "AstrBotDevs-demo-39386ee", | ||
| "owner-repo-branch/subdir/", | ||
| ".", | ||
| ], | ||
| ) | ||
| def test_plugin_unzip_file_normalizes_windows_extended_length_paths( | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| archive_root: str, | ||
| ) -> None: | ||
| import astrbot.core.star.updator as plugin_updator_module | ||
|
|
||
| target_dir = r"\\?\C:\Users\admin\AppData\Local\AstrBot\data\plugins\demo" | ||
| normalized_root = ntpath.normpath(archive_root) | ||
| expected_root = ( | ||
| target_dir | ||
| if normalized_root == "." | ||
| else ntpath.join(target_dir, normalized_root) | ||
| ) | ||
| expected_file = ntpath.join(expected_root, ".dockerignore") | ||
| captured: dict[str, object | None] = { | ||
| "listdir": None, | ||
| "move": None, | ||
| "cleanup": None, | ||
| "removed": None, | ||
| } | ||
|
|
||
| def fake_listdir(path: str) -> list[str]: | ||
| captured["listdir"] = path | ||
| return [".dockerignore"] | ||
|
|
||
| monkeypatch.setattr( | ||
| plugin_updator_module.os, "makedirs", lambda path, exist_ok=True: None | ||
| ) | ||
| monkeypatch.setattr(plugin_updator_module.os.path, "join", ntpath.join) | ||
| monkeypatch.setattr(plugin_updator_module.os.path, "normpath", ntpath.normpath) | ||
| monkeypatch.setattr(plugin_updator_module.os.path, "isdir", lambda path: False) | ||
| monkeypatch.setattr(plugin_updator_module.os.path, "exists", lambda path: False) | ||
| monkeypatch.setattr( | ||
| plugin_updator_module.zipfile, | ||
| "ZipFile", | ||
| lambda path, mode: _FakeZipArchive(_build_fake_archive_entries(archive_root)), | ||
| ) | ||
| monkeypatch.setattr(plugin_updator_module.logger, "info", lambda message: None) | ||
| monkeypatch.setattr(plugin_updator_module.logger, "warning", lambda message: None) | ||
| monkeypatch.setattr(plugin_updator_module.os, "listdir", fake_listdir) | ||
| monkeypatch.setattr( | ||
| plugin_updator_module.shutil, | ||
| "move", | ||
| lambda src, dst: captured.__setitem__("move", (src, dst)), | ||
| ) | ||
| monkeypatch.setattr( | ||
| plugin_updator_module.shutil, | ||
| "rmtree", | ||
| lambda path, onerror=None: captured.__setitem__("cleanup", path), | ||
| ) | ||
| monkeypatch.setattr( | ||
| plugin_updator_module.os, | ||
| "remove", | ||
| lambda path: captured.__setitem__("removed", path), | ||
| ) | ||
|
|
||
| PluginUpdator.__new__(PluginUpdator).unzip_file("temp.zip", target_dir) | ||
|
|
||
| assert captured["removed"] == "temp.zip" | ||
| if normalized_root == ".": | ||
| assert captured["listdir"] is None | ||
| assert captured["move"] is None | ||
| assert captured["cleanup"] is None | ||
| return | ||
|
|
||
| assert captured["listdir"] == expected_root | ||
| assert captured["move"] == (expected_file, target_dir) | ||
| assert captured["cleanup"] == expected_root | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catching
BaseExceptionis overly broad as it includes system-exiting exceptions likeSystemExitandKeyboardInterrupt. It is recommended to catchExceptioninstead to handle standard runtime errors during cleanup.