forked from bitcoinafterlife/bal-electrum-plugin
Read the plugin version at runtime from bal/manifest.json via importlib.resources (zip-safe, works from inside a zip on Windows). Removes the four-file duplication (VERSION, __init__.py, plugin_base.py, manifest.json) and deletes the bal/VERSION file. - bal/core/plugin_base.py: new get_version() function, BalPlugin.version is now a @property, removed hardcoded __version__ - bal/__init__.py: removed __version__, replaced with a comment - bal/core/willexecutors.py: user-agent uses get_version() - bal/gui/qt/dialogs.py, widgets.py: call sites use .version property - tests/test_version_source.py: 7 tests incl. zip-safe subprocess check - HANDOFF.md: updated to single-source-of-truth documentation - CHANGELOG.md: entries 43-46 - bal/manifest.json: version bumped to 0.6.1 PR #4 (branch bitcoinafterlife-patch-5).
117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
"""Tests for the single-source-of-truth plugin version.
|
|
|
|
The plugin version must come ONLY from ``bal/manifest.json`` (no hardcoded
|
|
copies, no ``bal/VERSION`` file), and it must be readable both from an extracted
|
|
package and from INSIDE a zip (the way Electrum loads external plugins via
|
|
zipimport). These tests lock that behavior.
|
|
|
|
Run:
|
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
|
|
python3 -m pytest tests/test_version_source.py -q
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import zipfile
|
|
|
|
import bal
|
|
from bal.core.plugin_base import BalPlugin, get_version
|
|
|
|
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
_MANIFEST = os.path.join(_REPO, "bal", "manifest.json")
|
|
|
|
|
|
def _manifest_version():
|
|
with open(_MANIFEST, encoding="utf-8") as f:
|
|
return json.load(f)["version"]
|
|
|
|
|
|
def test_get_version_matches_manifest():
|
|
"""get_version() returns exactly the manifest 'version' field."""
|
|
assert get_version() == _manifest_version()
|
|
|
|
|
|
def test_get_version_is_not_unknown():
|
|
"""The manifest must be readable - never the 'unknown' fallback here."""
|
|
assert get_version() != "unknown"
|
|
# a plausible semantic version, e.g. 0.6.0
|
|
assert get_version()[0].isdigit()
|
|
|
|
|
|
def test_version_property_matches_get_version():
|
|
"""BalPlugin.version is a property returning the same value."""
|
|
assert isinstance(BalPlugin.version, property)
|
|
# A bare object works as ``self`` because the property ignores instance
|
|
# state and simply delegates to get_version().
|
|
class _Dummy:
|
|
version = BalPlugin.version
|
|
assert _Dummy().version == get_version()
|
|
|
|
|
|
def test_no_hardcoded_class_version():
|
|
"""The old hardcoded BalPlugin.__version__ constant is gone."""
|
|
assert "__version__" not in vars(BalPlugin)
|
|
|
|
|
|
def test_no_version_file_in_package():
|
|
"""The bal/VERSION file has been removed (manifest is the only source)."""
|
|
assert not os.path.exists(os.path.join(_REPO, "bal", "VERSION"))
|
|
|
|
|
|
def test_get_version_cached():
|
|
"""Second call returns the cached value (same object)."""
|
|
v1 = get_version()
|
|
v2 = get_version()
|
|
assert v1 == v2
|
|
assert v1 is v2 # cached string, identical object
|
|
|
|
|
|
def test_version_readable_from_inside_zip(tmp_path):
|
|
"""The version must be readable when 'bal' is imported from a zip.
|
|
|
|
Electrum loads external plugins via zipimport, so this is the real
|
|
production scenario (and the one that used to break on Windows with
|
|
os.path.join). We build a zip of the bal package and read the version in a
|
|
fresh interpreter whose only path to 'bal' is that zip.
|
|
"""
|
|
bal_dir = os.path.dirname(os.path.abspath(bal.__file__))
|
|
zip_path = tmp_path / "bal_plugin.zip"
|
|
with zipfile.ZipFile(zip_path, "w") as z:
|
|
for root, _dirs, files in os.walk(bal_dir):
|
|
for fn in files:
|
|
if fn.endswith(".pyc"):
|
|
continue
|
|
full = os.path.join(root, fn)
|
|
# arcname keeps the leading 'bal/' package prefix
|
|
arc = os.path.join(
|
|
"bal", os.path.relpath(full, bal_dir)
|
|
)
|
|
z.write(full, arc)
|
|
|
|
# electrum must stay importable in the child, but 'bal' must resolve ONLY
|
|
# from the zip. So: drop the on-disk repo (and this package's dir) from the
|
|
# child's path, prepend the zip, and run from a neutral working directory.
|
|
repo_real = os.path.realpath(_REPO)
|
|
bal_parent_real = os.path.realpath(os.path.dirname(bal_dir))
|
|
filtered = [
|
|
p for p in sys.path
|
|
if p and os.path.realpath(p) not in (repo_real, bal_parent_real)
|
|
]
|
|
child_path = os.pathsep.join([str(zip_path)] + filtered)
|
|
env = dict(os.environ, PYTHONPATH=child_path, QT_QPA_PLATFORM="offscreen")
|
|
code = (
|
|
"import bal, os;"
|
|
"assert 'bal_plugin.zip' in bal.__file__.replace(os.sep, '/'),"
|
|
" 'bal not loaded from zip: ' + bal.__file__;"
|
|
"from bal.core.plugin_base import get_version;"
|
|
"print(get_version())"
|
|
)
|
|
out = subprocess.run(
|
|
[sys.executable, "-c", code], env=env, capture_output=True, text=True,
|
|
cwd=str(tmp_path),
|
|
)
|
|
assert out.returncode == 0, f"child failed: {out.stderr}"
|
|
assert out.stdout.strip() == _manifest_version()
|