Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion server.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,12 @@ def get_merged(self, reference_pitch: float = DEFAULT_REFERENCE_PITCH) -> dict:
for name, freqs in names.items():
result[instrument][name] = [round(f * scale, 4) for f in freqs]
except Exception:
logger.exception("tuning provider %r raised during get_merged()", provider_id)
# `log`, not `logger` — there is no `logger` in this module. This handler
# exists so ONE bad provider cannot break tunings for everyone; with the
# wrong name it raised NameError from inside the except and did precisely
# what it was written to prevent. See #899 and
# tests/test_tuning_provider_isolation.py.
log.exception("tuning provider %r raised during get_merged()", provider_id)
return result


Expand Down
72 changes: 72 additions & 0 deletions tests/test_tuning_provider_isolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""A raising tuning provider must not take down get_merged() for everyone. (#899)

`TuningProviderRegistry.get_merged()` wraps each provider in a try/except precisely so one
misbehaving plugin cannot break tunings for the rest. The handler called `logger.exception`
— and there is no `logger` in server.py; the module logger is `log`. So the handler MEANT
to swallow-and-report instead raised NameError, which propagated out of get_merged().

The net effect was the exact opposite of the handler's purpose: one bad provider took the
whole merged-tunings call down, and the traceback named the wrong problem.

Nothing exercised the failure path, which is why it survived. This is that path.
"""

import importlib
import logging
import sys

import pytest


@pytest.fixture()
def registry(monkeypatch, tmp_path):
monkeypatch.setenv("CONFIG_DIR", str(tmp_path))
sys.modules.pop("server", None)
mod = importlib.import_module("server")
yield mod.TuningProviderRegistry()
Comment on lines +21 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore the cached server module after the fixture.

Removing server from sys.modules and leaving the newly imported module cached can contaminate later tests: they may see a module initialized with the temporary CONFIG_DIR. Use monkeypatch.delitem(sys.modules, "server", raising=False) or explicitly restore the previous module during teardown.

Proposed fix
-    sys.modules.pop("server", None)
+    monkeypatch.delitem(sys.modules, "server", raising=False)
     mod = importlib.import_module("server")
     yield mod.TuningProviderRegistry()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@pytest.fixture()
def registry(monkeypatch, tmp_path):
monkeypatch.setenv("CONFIG_DIR", str(tmp_path))
sys.modules.pop("server", None)
mod = importlib.import_module("server")
yield mod.TuningProviderRegistry()
`@pytest.fixture`()
def registry(monkeypatch, tmp_path):
monkeypatch.setenv("CONFIG_DIR", str(tmp_path))
monkeypatch.delitem(sys.modules, "server", raising=False)
mod = importlib.import_module("server")
yield mod.TuningProviderRegistry()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_tuning_provider_isolation.py` around lines 21 - 26, Update the
registry fixture’s module-isolation teardown: after importing and yielding the
registry, restore the prior sys.modules["server"] entry or remove the
fixture-imported module via monkeypatch so later tests do not retain the
temporary CONFIG_DIR initialization. Keep the existing TuningProviderRegistry
setup unchanged.



def test_a_raising_provider_does_not_break_the_others(registry, caplog):
"""The whole point of the try/except. Before the fix this raised NameError."""
def boom():
raise RuntimeError("provider exploded")

def good():
return {"guitar": {"My Tuning": [82.41, 110.0, 146.83, 196.0, 246.94, 329.63]}}

registry.register("bad-plugin", boom)
registry.register("good-plugin", good)

merged = registry.get_merged() # must NOT raise

assert "My Tuning" in merged["guitar"], (
"the healthy provider's tuning is missing — one raising provider took down the "
"merged result for everyone"
)
# and the default tunings survive
assert merged["guitar"], "default tunings were lost"
Comment on lines +42 to +47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that a known default tuning survives.

assert merged["guitar"] is vacuous because the healthy provider already adds "My Tuning". The test would pass even if all defaults were lost, so it does not cover the stated default-preservation objective.

Proposed fix
+    default_guitar_names = set(registry.get_merged()["guitar"])
     registry.register("bad-plugin", boom)
     registry.register("good-plugin", good)

     merged = registry.get_merged()
...
-    # and the default tunings survive
-    assert merged["guitar"], "default tunings were lost"
+    assert default_guitar_names <= merged["guitar"].keys(), (
+        "default tunings were lost"
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert "My Tuning" in merged["guitar"], (
"the healthy provider's tuning is missing — one raising provider took down the "
"merged result for everyone"
)
# and the default tunings survive
assert merged["guitar"], "default tunings were lost"
default_guitar_names = set(registry.get_merged()["guitar"])
registry.register("bad-plugin", boom)
registry.register("good-plugin", good)
merged = registry.get_merged()
assert "My Tuning" in merged["guitar"], (
"the healthy provider's tuning is missing — one raising provider took down the "
"merged result for everyone"
)
assert default_guitar_names <= merged["guitar"].keys(), (
"default tunings were lost"
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_tuning_provider_isolation.py` around lines 42 - 47, Update the
default-preservation assertion in the tuning provider isolation test to check
for a specific known default tuning value, rather than only asserting that
merged["guitar"] is non-empty. Keep the existing healthy-provider assertion
unchanged and use the repository’s established default tuning name.



def test_the_failure_is_actually_logged(registry, caplog):
"""Swallowing is only acceptable if it is reported. A NameError in the handler meant
nothing was ever logged — the failure was both fatal AND silent about its real cause."""
def boom():
raise RuntimeError("provider exploded")

registry.register("bad-plugin", boom)

# The feedBack logger sets propagate=False, so pytest's root-logger capture sees
# NOTHING from it. Attach caplog's handler directly. (test_plugins.py has a
# capture_logger() context manager for this, but it is not importable from here:
# pyproject pins pythonpath to [".", "lib"], so `tests` is not a package.)
lg = logging.getLogger("feedBack")
lg.addHandler(caplog.handler)
lg.setLevel(logging.ERROR)
try:
registry.get_merged()
finally:
lg.removeHandler(caplog.handler)
Comment on lines +62 to +68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore the feedBack logger level.

The test sets a process-global logger level to ERROR and never restores it, which can make later tests order-dependent.

Proposed fix
+    previous_level = lg.level
     lg.addHandler(caplog.handler)
     lg.setLevel(logging.ERROR)
     try:
         registry.get_merged()
     finally:
         lg.removeHandler(caplog.handler)
+        lg.setLevel(previous_level)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lg = logging.getLogger("feedBack")
lg.addHandler(caplog.handler)
lg.setLevel(logging.ERROR)
try:
registry.get_merged()
finally:
lg.removeHandler(caplog.handler)
previous_level = lg.level
lg.addHandler(caplog.handler)
lg.setLevel(logging.ERROR)
try:
registry.get_merged()
finally:
lg.removeHandler(caplog.handler)
lg.setLevel(previous_level)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_tuning_provider_isolation.py` around lines 62 - 68, Restore the
original level of the feedBack logger after the registry.get_merged() assertion
completes. Capture the logger’s level before setting it to ERROR, then restore
that saved level in the existing finally block alongside removing
caplog.handler.


assert any("bad-plugin" in r.getMessage() for r in caplog.records), (
"the raising provider was never named in the logs"
)
Loading