mirror of
https://github.com/beetbox/beets.git
synced 2026-07-21 21:27:01 -04:00
- Removing the coverage omit configuration exposed unused branches, helpers, and fixtures across the test suite. - Delete that redundant logic and simplify affected tests while preserving their behavior.
506 lines
18 KiB
Python
506 lines
18 KiB
Python
"""Tests for the command-line interface."""
|
|
|
|
import os
|
|
import platform
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from confuse import ConfigError
|
|
|
|
from beets import config, plugins, ui
|
|
from beets.exceptions import UserError
|
|
from beets.test import _common
|
|
from beets.test.helper import BeetsTestCase, IOMixin, PluginTestCase
|
|
from beets.ui import _open_library, commands
|
|
from beets.util import syspath
|
|
|
|
|
|
class PrintTest(IOMixin, unittest.TestCase):
|
|
def test_print_without_locale(self):
|
|
with patch.dict(os.environ, {}, clear=False):
|
|
os.environ.pop("LANG", None)
|
|
|
|
ui.print_("something")
|
|
|
|
@patch.dict(os.environ, {"LANG": "", "LC_CTYPE": "UTF-8"}, clear=False)
|
|
def test_print_with_invalid_locale(self):
|
|
ui.print_("something")
|
|
|
|
|
|
class ShowModelChangesTest(IOMixin, BeetsTestCase):
|
|
def test_uses_database_state_when_old_not_provided(self):
|
|
item = self.add_item_fixture(title="old title")
|
|
old_label = format(item.get_fresh_from_db())
|
|
|
|
item.title = "new title"
|
|
|
|
assert ui.show_model_changes(item) is True
|
|
assert self.io.getoutput().splitlines() == [
|
|
old_label,
|
|
" title: old title -> new title",
|
|
]
|
|
|
|
|
|
class TestPluginTestCase(PluginTestCase):
|
|
plugin = "test"
|
|
|
|
def setUp(self):
|
|
self.config["pluginpath"] = [_common.PLUGINPATH]
|
|
super().setUp()
|
|
|
|
|
|
class ConfigTest(IOMixin, TestPluginTestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
|
|
# Don't use the BEETSDIR from `helper`. Instead, we point the home
|
|
# directory there. Some tests will set `BEETSDIR` themselves.
|
|
del os.environ["BEETSDIR"]
|
|
|
|
# Also set APPDATA, the Windows equivalent of setting $HOME.
|
|
appdata_dir = self.temp_dir_path / "AppData" / "Roaming"
|
|
|
|
self._orig_cwd = os.getcwd()
|
|
self.test_cmd = self._make_test_cmd()
|
|
commands.default_commands.append(self.test_cmd)
|
|
|
|
# Default user configuration
|
|
if platform.system() == "Windows":
|
|
self.user_config_dir = appdata_dir / "beets"
|
|
else:
|
|
self.user_config_dir = self.temp_dir_path / ".config" / "beets"
|
|
self.user_config_dir.mkdir(parents=True, exist_ok=True)
|
|
self.user_config_path = self.user_config_dir / "config.yaml"
|
|
|
|
# Custom BEETSDIR
|
|
self.beetsdir = self.temp_dir_path / "beetsdir"
|
|
self.beetsdir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.env_config_path = str(self.beetsdir / "config.yaml")
|
|
self.cli_config_path = str(self.temp_dir_path / "config.yaml")
|
|
self.env_patcher = patch(
|
|
"os.environ",
|
|
{"HOME": str(self.temp_dir_path), "APPDATA": str(appdata_dir)},
|
|
)
|
|
self.env_patcher.start()
|
|
|
|
self._reset_config()
|
|
|
|
def run_command(self, *args, **kwargs):
|
|
# We need to reload the lib based on the
|
|
# current config
|
|
self.lib = _open_library(self.config)
|
|
super().run_command(*args, **kwargs)
|
|
|
|
def tearDown(self):
|
|
self.env_patcher.stop()
|
|
commands.default_commands.pop()
|
|
os.chdir(syspath(self._orig_cwd))
|
|
super().tearDown()
|
|
|
|
def _make_test_cmd(self):
|
|
test_cmd = ui.Subcommand("test", help="test")
|
|
|
|
def run(lib, options, args):
|
|
test_cmd.lib = lib
|
|
test_cmd.options = options
|
|
test_cmd.args = args
|
|
|
|
test_cmd.func = run
|
|
return test_cmd
|
|
|
|
def _reset_config(self):
|
|
# Config should read files again on demand
|
|
config.clear()
|
|
config._materialized = False
|
|
|
|
def write_config_file(self):
|
|
return open(self.user_config_path, "w")
|
|
|
|
def test_paths_section_respected(self):
|
|
with self.write_config_file() as config:
|
|
config.write("paths: {x: y}")
|
|
|
|
self.run_command("test")
|
|
key, template = self.test_cmd.lib.path_formats[0]
|
|
assert key == "x"
|
|
assert template.original == "y"
|
|
|
|
def test_nonexistant_db(self):
|
|
with self.write_config_file() as config:
|
|
config.write("library: /xxx/yyy/not/a/real/path")
|
|
|
|
self.io.addinput("n")
|
|
with pytest.raises(UserError):
|
|
self.run_command("test")
|
|
|
|
def test_user_config_file(self):
|
|
with self.write_config_file() as file:
|
|
file.write("anoption: value")
|
|
|
|
self.run_command("test")
|
|
assert config["anoption"].get() == "value"
|
|
|
|
def test_replacements_parsed(self):
|
|
with self.write_config_file() as config:
|
|
config.write("replace: {'[xy]': z}")
|
|
|
|
self.run_command("test")
|
|
replacements = self.test_cmd.lib.replacements
|
|
repls = [(p.pattern, s) for p, s in replacements] # Compare patterns.
|
|
assert repls == [("[xy]", "z")]
|
|
|
|
def test_multiple_replacements_parsed(self):
|
|
with self.write_config_file() as config:
|
|
config.write("replace: {'[xy]': z, foo: bar}")
|
|
self.run_command("test")
|
|
replacements = self.test_cmd.lib.replacements
|
|
repls = [(p.pattern, s) for p, s in replacements]
|
|
assert repls == [("[xy]", "z"), ("foo", "bar")]
|
|
|
|
def test_cli_config_option(self):
|
|
with open(self.cli_config_path, "w") as file:
|
|
file.write("anoption: value")
|
|
self.run_command("--config", self.cli_config_path, "test")
|
|
assert config["anoption"].get() == "value"
|
|
|
|
def test_cli_config_file_overwrites_user_defaults(self):
|
|
with open(self.user_config_path, "w") as file:
|
|
file.write("anoption: value")
|
|
|
|
with open(self.cli_config_path, "w") as file:
|
|
file.write("anoption: cli overwrite")
|
|
self.run_command("--config", self.cli_config_path, "test")
|
|
assert config["anoption"].get() == "cli overwrite"
|
|
|
|
def test_cli_config_file_overwrites_beetsdir_defaults(self):
|
|
os.environ["BEETSDIR"] = str(self.beetsdir)
|
|
with open(self.env_config_path, "w") as file:
|
|
file.write("anoption: value")
|
|
|
|
with open(self.cli_config_path, "w") as file:
|
|
file.write("anoption: cli overwrite")
|
|
self.run_command("--config", self.cli_config_path, "test")
|
|
assert config["anoption"].get() == "cli overwrite"
|
|
|
|
# @unittest.skip('Difficult to implement with optparse')
|
|
# def test_multiple_cli_config_files(self):
|
|
# cli_config_path_1 = os.path.join(self.temp_dir, b'config.yaml')
|
|
# cli_config_path_2 = os.path.join(self.temp_dir, b'config_2.yaml')
|
|
#
|
|
# with open(cli_config_path_1, 'w') as file:
|
|
# file.write('first: value')
|
|
#
|
|
# with open(cli_config_path_2, 'w') as file:
|
|
# file.write('second: value')
|
|
#
|
|
# self.run_command('--config', cli_config_path_1,
|
|
# '--config', cli_config_path_2, 'test')
|
|
# assert config['first'].get() == 'value'
|
|
# assert config['second'].get() == 'value'
|
|
#
|
|
# @unittest.skip('Difficult to implement with optparse')
|
|
# def test_multiple_cli_config_overwrite(self):
|
|
# cli_overwrite_config_path = os.path.join(self.temp_dir,
|
|
# b'overwrite_config.yaml')
|
|
#
|
|
# with open(self.cli_config_path, 'w') as file:
|
|
# file.write('anoption: value')
|
|
#
|
|
# with open(cli_overwrite_config_path, 'w') as file:
|
|
# file.write('anoption: overwrite')
|
|
#
|
|
# self.run_command('--config', self.cli_config_path,
|
|
# '--config', cli_overwrite_config_path, 'test')
|
|
# assert config['anoption'].get() == 'cli overwrite'
|
|
|
|
# FIXME: fails on windows
|
|
@unittest.skipIf(sys.platform == "win32", "win32")
|
|
def test_cli_config_paths_resolve_relative_to_user_dir(self):
|
|
with open(self.cli_config_path, "w") as file:
|
|
file.write("library: beets.db\n")
|
|
file.write("statefile: state")
|
|
|
|
self.run_command("--config", self.cli_config_path, "test")
|
|
assert config["library"].as_path() == self.user_config_dir / "beets.db"
|
|
assert config["statefile"].as_path() == self.user_config_dir / "state"
|
|
|
|
def test_cli_config_paths_resolve_relative_to_beetsdir(self):
|
|
os.environ["BEETSDIR"] = str(self.beetsdir)
|
|
|
|
with open(self.cli_config_path, "w") as file:
|
|
file.write("library: beets.db\n")
|
|
file.write("statefile: state")
|
|
|
|
self.run_command("--config", self.cli_config_path, "test")
|
|
assert config["library"].as_path() == self.beetsdir / "beets.db"
|
|
assert config["statefile"].as_path() == self.beetsdir / "state"
|
|
|
|
def test_command_line_option_relative_to_working_dir(self):
|
|
config.read()
|
|
os.chdir(syspath(self.temp_dir))
|
|
self.run_command("--library", "foo.db", "test")
|
|
assert config["library"].as_path() == Path.cwd() / "foo.db"
|
|
|
|
def test_cli_config_file_loads_plugin_commands(self):
|
|
with open(self.cli_config_path, "w") as file:
|
|
file.write(f"pluginpath: {_common.PLUGINPATH}\n")
|
|
file.write("plugins: test")
|
|
|
|
self.run_command("--config", self.cli_config_path, "plugin")
|
|
plugs = plugins.find_plugins()
|
|
assert len(plugs) == 1
|
|
assert plugs[0].is_test_plugin
|
|
self.unload_plugins()
|
|
|
|
def test_beetsdir_config(self):
|
|
os.environ["BEETSDIR"] = str(self.beetsdir)
|
|
|
|
with open(self.env_config_path, "w") as file:
|
|
file.write("anoption: overwrite")
|
|
|
|
config.read()
|
|
assert config["anoption"].get() == "overwrite"
|
|
|
|
def test_beetsdir_points_to_file_error(self):
|
|
beetsdir = str(self.temp_dir_path / "beetsfile")
|
|
open(beetsdir, "a").close()
|
|
os.environ["BEETSDIR"] = beetsdir
|
|
with pytest.raises(ConfigError):
|
|
self.run_command("test")
|
|
|
|
def test_beetsdir_config_does_not_load_default_user_config(self):
|
|
os.environ["BEETSDIR"] = str(self.beetsdir)
|
|
|
|
with open(self.user_config_path, "w") as file:
|
|
file.write("anoption: value")
|
|
|
|
config.read()
|
|
assert not config["anoption"].exists()
|
|
|
|
def test_default_config_paths_resolve_relative_to_beetsdir(self):
|
|
os.environ["BEETSDIR"] = str(self.beetsdir)
|
|
|
|
config.read()
|
|
assert config["library"].as_path() == self.beetsdir / "library.db"
|
|
assert config["statefile"].as_path() == self.beetsdir / "state.pickle"
|
|
|
|
def test_beetsdir_config_paths_resolve_relative_to_beetsdir(self):
|
|
os.environ["BEETSDIR"] = str(self.beetsdir)
|
|
|
|
with open(self.env_config_path, "w") as file:
|
|
file.write("library: beets.db\n")
|
|
file.write("statefile: state")
|
|
|
|
config.read()
|
|
assert config["library"].as_path() == self.beetsdir / "beets.db"
|
|
assert config["statefile"].as_path() == self.beetsdir / "state"
|
|
|
|
|
|
class PluginTest(TestPluginTestCase):
|
|
def test_plugin_command_from_pluginpath(self):
|
|
self.run_command("test")
|
|
|
|
|
|
class CommonOptionsParserCliTest(IOMixin, BeetsTestCase):
|
|
"""Test CommonOptionsParser and formatting LibModel formatting on 'list'
|
|
command.
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.item = _common.item()
|
|
self.item.path = b"xxx/yyy"
|
|
self.lib.add(self.item)
|
|
self.lib.add_album([self.item])
|
|
|
|
def test_base(self):
|
|
output = self.run_with_output("ls")
|
|
assert output == "the artist - the album - the title\n"
|
|
|
|
output = self.run_with_output("ls", "-a")
|
|
assert output == "the album artist - the album\n"
|
|
|
|
def test_path_option(self):
|
|
output = self.run_with_output("ls", "-p")
|
|
assert output == f"{self.lib_path / 'xxx/yyy'}\n"
|
|
|
|
output = self.run_with_output("ls", "-a", "-p")
|
|
assert output == f"{self.lib_path / 'xxx'}\n"
|
|
|
|
def test_format_option(self):
|
|
output = self.run_with_output("ls", "-f", "$artist")
|
|
assert output == "the artist\n"
|
|
|
|
output = self.run_with_output("ls", "-a", "-f", "$albumartist")
|
|
assert output == "the album artist\n"
|
|
|
|
def test_format_option_unicode(self):
|
|
output = self.run_with_output("ls", "-f", "caf\xe9")
|
|
assert output == "caf\xe9\n"
|
|
|
|
def test_root_format_option(self):
|
|
output = self.run_with_output(
|
|
"--format-item", "$artist", "--format-album", "foo", "ls"
|
|
)
|
|
assert output == "the artist\n"
|
|
|
|
output = self.run_with_output(
|
|
"--format-item", "foo", "--format-album", "$albumartist", "ls", "-a"
|
|
)
|
|
assert output == "the album artist\n"
|
|
|
|
def test_help(self):
|
|
output = self.run_with_output("help")
|
|
assert "Usage:" in output
|
|
|
|
# command description is on the same line
|
|
assert (
|
|
"config show or edit the user configuration" in output
|
|
), "unexpected command description formatting"
|
|
|
|
output = self.run_with_output("help", "list")
|
|
assert "Usage:" in output
|
|
|
|
with pytest.raises(UserError):
|
|
self.run_command("help", "this.is.not.a.real.command")
|
|
|
|
def test_stats(self):
|
|
output = self.run_with_output("stats")
|
|
assert "Approximate total size:" in output
|
|
|
|
# # Need to have more realistic library setup for this to work
|
|
# output = self.run_with_output('stats', '-e')
|
|
# assert 'Total size:' in output
|
|
|
|
def test_version(self):
|
|
output = self.run_with_output("version")
|
|
assert "Python version" in output
|
|
assert "no plugins loaded" in output
|
|
|
|
# # Need to have plugin loaded
|
|
# output = self.run_with_output('version')
|
|
# assert 'plugins: ' in output
|
|
|
|
|
|
class CommonOptionsParserTest(unittest.TestCase):
|
|
def test_album_option(self):
|
|
parser = ui.CommonOptionsParser()
|
|
assert not parser._album_flags
|
|
parser.add_album_option()
|
|
assert bool(parser._album_flags)
|
|
|
|
assert parser.parse_args([]) == ({"album": None}, [])
|
|
assert parser.parse_args(["-a"]) == ({"album": True}, [])
|
|
assert parser.parse_args(["--album"]) == ({"album": True}, [])
|
|
|
|
def test_path_option(self):
|
|
parser = ui.CommonOptionsParser()
|
|
parser.add_path_option()
|
|
assert not parser._album_flags
|
|
|
|
config["format_item"].set("$foo")
|
|
assert parser.parse_args([]) == ({"path": None}, [])
|
|
assert config["format_item"].as_str() == "$foo"
|
|
|
|
assert parser.parse_args(["-p"]) == (
|
|
{"path": True, "format": "$path"},
|
|
[],
|
|
)
|
|
assert parser.parse_args(["--path"]) == (
|
|
{"path": True, "format": "$path"},
|
|
[],
|
|
)
|
|
|
|
assert config["format_item"].as_str() == "$path"
|
|
assert config["format_album"].as_str() == "$path"
|
|
|
|
def test_format_option(self):
|
|
parser = ui.CommonOptionsParser()
|
|
parser.add_format_option()
|
|
assert not parser._album_flags
|
|
|
|
config["format_item"].set("$foo")
|
|
assert parser.parse_args([]) == ({"format": None}, [])
|
|
assert config["format_item"].as_str() == "$foo"
|
|
|
|
assert parser.parse_args(["-f", "$bar"]) == ({"format": "$bar"}, [])
|
|
assert parser.parse_args(["--format", "$baz"]) == (
|
|
{"format": "$baz"},
|
|
[],
|
|
)
|
|
|
|
assert config["format_item"].as_str() == "$baz"
|
|
assert config["format_album"].as_str() == "$baz"
|
|
|
|
def test_format_option_with_target(self):
|
|
with pytest.raises(KeyError):
|
|
ui.CommonOptionsParser().add_format_option(target="thingy")
|
|
|
|
parser = ui.CommonOptionsParser()
|
|
parser.add_format_option(target="item")
|
|
|
|
config["format_item"].set("$item")
|
|
config["format_album"].set("$album")
|
|
|
|
assert parser.parse_args(["-f", "$bar"]) == ({"format": "$bar"}, [])
|
|
|
|
assert config["format_item"].as_str() == "$bar"
|
|
assert config["format_album"].as_str() == "$album"
|
|
|
|
def test_format_option_with_album(self):
|
|
parser = ui.CommonOptionsParser()
|
|
parser.add_album_option()
|
|
parser.add_format_option()
|
|
|
|
config["format_item"].set("$item")
|
|
config["format_album"].set("$album")
|
|
|
|
parser.parse_args(["-f", "$bar"])
|
|
assert config["format_item"].as_str() == "$bar"
|
|
assert config["format_album"].as_str() == "$album"
|
|
|
|
parser.parse_args(["-a", "-f", "$foo"])
|
|
assert config["format_item"].as_str() == "$bar"
|
|
assert config["format_album"].as_str() == "$foo"
|
|
|
|
parser.parse_args(["-f", "$foo2", "-a"])
|
|
assert config["format_album"].as_str() == "$foo2"
|
|
|
|
def test_add_all_common_options(self):
|
|
parser = ui.CommonOptionsParser()
|
|
parser.add_all_common_options()
|
|
assert parser.parse_args([]) == (
|
|
{"album": None, "path": None, "format": None},
|
|
[],
|
|
)
|
|
|
|
|
|
class TestEncoding:
|
|
"""Tests for the `terminal_encoding` config option and our
|
|
`_in_encoding` and `_out_encoding` utility functions.
|
|
"""
|
|
|
|
def test_out_encoding_overridden(self, config):
|
|
config["terminal_encoding"] = "fake_encoding"
|
|
assert ui._out_encoding() == "fake_encoding"
|
|
|
|
def test_in_encoding_overridden(self, config):
|
|
config["terminal_encoding"] = "fake_encoding"
|
|
assert ui._in_encoding() == "fake_encoding"
|
|
|
|
def test_out_encoding_default_utf8(self, config):
|
|
config["terminal_encoding"] = None
|
|
with patch("sys.stdout") as stdout:
|
|
stdout.encoding = None
|
|
assert ui._out_encoding() == "utf-8"
|
|
|
|
def test_in_encoding_default_utf8(self, config):
|
|
config["terminal_encoding"] = None
|
|
with patch("sys.stdin") as stdin:
|
|
stdin.encoding = None
|
|
assert ui._in_encoding() == "utf-8"
|