Backup restore logging drops cog names and raises TypeError for failed libraries
#6792 · open · 1 comments
### What Red version are you using?
3.5.25.dev1 (V3/develop @ 61484f28f7fcfff81309cb2b0b0d5287c921f1ad)
### What were you trying to do?
Restore third-party cogs from a Red backup with `redbot-setup restore`. I audited the failure reporting in `redbot.core._downloader._restore_from_backup()` because these messages are the only diagnostic output when a cog cannot be restored.
### What did you expect to happen?
Every restore failure should produce a normal log record containing the cog name, repository name, and failed shared-library names. Logging a recovery failure must not itself raise a formatting exception.
### What actually happened?
Three log calls in the restore path have missing format arguments.
- Lines 989 and 994 log `The commit that %r cog...` without passing `cog.name`, so the literal placeholder is printed instead of the affected cog.
- The `failed_libs` branch at lines 1066-1069 has three placeholders but passes only two arguments. Python logging raises `TypeError: not enough arguments for format string`, and the intended failure message is lost.
Observed output:
WARNING:The commit that %r cog was installed from is unknown ...
ERROR:The commit that %r cog was installed from is unknown ...
--- Logging error ---
TypeError: not enough arguments for format string
Message: 'Failed to reinstall shared libraries for %r cog from %r repo: %s'
Arguments: ('repo-name', 'lib-name')
### How can we reproduce this error?
1. Check out `V3/develop` at `61484f28f7fcfff81309cb2b0b0d5287c921f1ad` and install the test dependencies on Python 3.11.
2. Attach a `logging.StreamHandler` to `red.core.downloader`.
3. Exercise the two no-commit branches in `_restore_from_backup()`; both records retain a literal `%r`.
4. Return a `CogInstallResult` with a non-empty `failed_libs` tuple from `install_cogs()`.
5. Observe the logging traceback above instead of the intended cog/repository/library diagnostic.
The same failure can be isolated with the production format string:
log.error(
'Failed to reinstall shared libraries for %r cog from %r repo: %s',
'repo-name',
'lib-name',
)
### Anything else?
Environment: Debian Bookworm arm64 container, Python 3.11.15. The unmodified baseline passes `313 passed, 7 skipped`; `pylint --errors-only redbot` independently reports E1206 at line 1066. I searched open and closed issues for the exact messages, backup restore logging errors, failed shared libraries, and missing commit metadata; no duplicate was found.
Likely minimal fix: pass `cog.name` to both missing-commit messages, and pass `cog.name`, `cog.repo.name`, then the joined library names to the failed-libraries message. I will add regression coverage for all three formatted records separately.
Implemented and verified separately after filing.
### Fix
- Passes the missing cog name to both no-commit warning/error records.
- Passes cog name, repository name, and joined library names to the failed-shared-libraries record.
- Adds a regression test that drives all three real _restore_from_backup() branches with isolated config, repository, progress, and installer fakes.
### Before/after proof
- Fresh unmodified V3/develop at 61484f28f7fcfff81309cb2b0b0d5287c921f1ad plus the regression: **1 failed**, reproducing TypeError: not enough arguments for format string; the other two captured records contain literal %r.
- Patched focused regression: **1 passed**.
- Patched full suite: **314 passed, 7 skipped**.
- python -m compileall -q redbot: passed.
- black --check redbot tests: passed; **231 files unchanged**.
- pylint --errors-only redbot/core/_downloader/__init__.py: exit 0; the prior E1206 is gone.
- git diff --check: passed.
- The patch below passed git apply --check in a fresh clone at the exact reported commit.
Tested on Debian Bookworm arm64 with Python 3.11.15. No Discord token or live bot connection is required for this restore-path test.
### Apply and verify
```bash
git checkout 61484f28f7fcfff81309cb2b0b0d5287c921f1ad
git apply red-discordbot-issue-6792-fix.patch
pytest -q tests/core/_downloader/test_restore.py
pytest -q
```
<details>
<summary>Complete apply-ready patch</summary>
```diff
diff --git a/redbot/core/_downloader/__init__.py b/redbot/core/_downloader/__init__.py
index 03cadc9..48305b1 100644
--- a/redbot/core/_downloader/__init__.py
+++ b/redbot/core/_downloader/__init__.py
@@ -988,12 +988,14 @@ async def _restore_from_backup() -> None:
cog = last_cog_occurrence
log.warning(
"The commit that %r cog was installed from is unknown"
- " - will try to reinstall from latest commit where it's still available."
+ " - will try to reinstall from latest commit where it's still available.",
+ cog.name,
)
else:
log.error(
"The commit that %r cog was installed from is unknown"
- " and it could not be found in the repo."
+ " and it could not be found in the repo.",
+ cog.name,
)
continue
@@ -1065,6 +1067,7 @@ async def _restore_from_backup() -> None:
if install_result.failed_libs:
log.error(
"Failed to reinstall shared libraries for %r cog from %r repo: %s",
+ cog.name,
cog.repo.name,
", ".join([lib.name for lib in install_result.failed_libs]),
)
diff --git a/tests/core/_downloader/test_restore.py b/tests/core/_downloader/test_restore.py
new file mode 100644
index 0000000..2de3916
--- /dev/null
+++ b/tests/core/_downloader/test_restore.py
@@ -0,0 +1,100 @@
+import contextlib
+import logging
+from types import SimpleNamespace
+from unittest.mock import AsyncMock
+
+from redbot.core import _downloader as downloader
+
+
+class FakeProgress:
+ def add_task(self, description):
+ return 1
+
+ def track(self, sequence, *, task_id):
+ return sequence
+
+ def update(self, task_id, *, description):
+ pass
+
+
[email protected]
+def fake_detailed_progress(*, unit):
+ yield FakeProgress()
+
+
+def install_result(*, failed_libs=()):
+ return downloader.CogInstallResult(
+ installed_cogs=(),
+ installed_libs=(),
+ failed_cogs=(),
+ failed_libs=failed_libs,
+ failed_reqs=(),
+ unavailable_cogs=(),
+ already_installed=(),
+ name_already_used=(),
+ incompatible_python_version=(),
+ incompatible_bot_version=(),
+ )
+
+
+async def test_restore_failure_logs_include_cog_and_repo_names(monkeypatch, tmp_path, caplog):
+ (tmp_path / "repos.json").write_text("[]", encoding="utf-8")
+
+ fallback_repo = SimpleNamespace(
+ name="fallback-repo",
+ get_last_module_occurrence=AsyncMock(),
+ )
+ missing_repo = SimpleNamespace(
+ name="missing-repo",
+ get_last_module_occurrence=AsyncMock(return_value=None),
+ )
+ failed_libs_repo = SimpleNamespace(name="failed-libs-repo")
+
+ fallback_cog = SimpleNamespace(name="fallback-cog", commit=None, repo=fallback_repo)
+ fallback_repo.get_last_module_occurrence.return_value = SimpleNamespace(
+ name="fallback-cog",
+ commit="fallback-commit",
+ disabled=False,
+ repo=fallback_repo,
+ )
+ missing_cog = SimpleNamespace(name="missing-cog", commit=None, repo=missing_repo)
+ failed_libs_cog = SimpleNamespace(
+ name="failed-libs-cog",
+ commit="failed-libs-commit",
+ repo=failed_libs_repo,
+ )
+
+ async def fake_install_cogs(repo, commit, cog_names):
+ if cog_names == ["failed-libs-cog"]:
+ return install_result(
+ failed_libs=(SimpleNamespace(name="lib-one"), SimpleNamespace(name="lib-two"))
+ )
+ return install_result()
+
+ fake_config = SimpleNamespace(
+ installed_cogs=SimpleNamespace(clear=AsyncMock()),
+ installed_libraries=SimpleNamespace(clear=AsyncMock()),
+ )
+ fake_repo_manager = SimpleNamespace(repos_folder=tmp_path / "repos")
+
+ monkeypatch.setattr(downloader, "_config", fake_config, raising=False)
+ monkeypatch.setattr(downloader, "_repo_manager", fake_repo_manager, raising=False)
+ monkeypatch.setattr(downloader, "detailed_progress", fake_detailed_progress)
+ monkeypatch.setattr(
+ downloader,
+ "installed_cogs",
+ AsyncMock(return_value=(fallback_cog, missing_cog, failed_libs_cog)),
+ )
+ monkeypatch.setattr(downloader, "install_cogs", fake_install_cogs)
+ caplog.set_level(logging.WARNING, logger=downloader.log.name)
+
+ await downloader._restore_from_backup()
+
+ assert caplog.messages == [
+ "The commit that 'fallback-cog' cog was installed from is unknown"
+ " - will try to reinstall from latest commit where it's still available.",
+ "The commit that 'missing-cog' cog was installed from is unknown"
+ " and it could not be found in the repo.",
+ "Failed to reinstall shared libraries for 'failed-libs-cog' cog"
+ " from 'failed-libs-repo' repo: lib-one, lib-two",
+ ]
```
</details>