beets/test/ui/test_ui.py
Sebastian Mohr a59e41a883 tests: move command tests into dedicated files
Moved tests related to ui into own folder.
Moved 'modify' command tests into own file.
Moved 'write' command tests into own file.
Moved 'fields' command tests into own file.
Moved 'do_query' test into own file.
Moved 'list' command tests into own file.
Moved 'remove' command tests into own file.
Moved 'move' command tests into own file.
Moved 'update' command tests into own file.
Moved 'show_change' test into test_import file.
Moved 'summarize_items' test into test_import file.
Moved 'completion' command test into own file.
2025-11-03 14:00:58 +01:00

590 lines
20 KiB
Python

# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""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.test import _common
from beets.test.helper import BeetsTestCase, IOMixin, PluginTestCase
from beets.ui import commands
from beets.util import syspath
class PrintTest(IOMixin, unittest.TestCase):
def test_print_without_locale(self):
lang = os.environ.get("LANG")
if lang:
del os.environ["LANG"]
try:
ui.print_("something")
except TypeError:
self.fail("TypeError during print")
finally:
if lang:
os.environ["LANG"] = lang
def test_print_with_invalid_locale(self):
old_lang = os.environ.get("LANG")
os.environ["LANG"] = ""
old_ctype = os.environ.get("LC_CTYPE")
os.environ["LC_CTYPE"] = "UTF-8"
try:
ui.print_("something")
except ValueError:
self.fail("ValueError during print")
finally:
if old_lang:
os.environ["LANG"] = old_lang
else:
del os.environ["LANG"]
if old_ctype:
os.environ["LC_CTYPE"] = old_ctype
else:
del os.environ["LC_CTYPE"]
@_common.slow_test()
class TestPluginTestCase(PluginTestCase):
plugin = "test"
def setUp(self):
super().setUp()
config["pluginpath"] = [_common.PLUGINPATH]
class ConfigTest(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 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", lib=None)
key, template = self.test_cmd.lib.path_formats[0]
assert key == "x"
assert template.original == "y"
def test_default_paths_preserved(self):
default_formats = ui.get_path_formats()
self._reset_config()
with self.write_config_file() as config:
config.write("paths: {x: y}")
self.run_command("test", lib=None)
key, template = self.test_cmd.lib.path_formats[0]
assert key == "x"
assert template.original == "y"
assert self.test_cmd.lib.path_formats[1:] == default_formats
def test_nonexistant_db(self):
with self.write_config_file() as config:
config.write("library: /xxx/yyy/not/a/real/path")
with pytest.raises(ui.UserError):
self.run_command("test", lib=None)
def test_user_config_file(self):
with self.write_config_file() as file:
file.write("anoption: value")
self.run_command("test", lib=None)
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", lib=None)
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", lib=None)
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", lib=None)
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", lib=None)
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", lib=None)
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', lib=None)
# 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", lib=None)
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", lib=None)
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", lib=None)
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", lib=None)
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 ShowModelChangeTest(IOMixin, unittest.TestCase):
def setUp(self):
super().setUp()
self.a = _common.item()
self.b = _common.item()
self.a.path = self.b.path
def _show(self, **kwargs):
change = ui.show_model_changes(self.a, self.b, **kwargs)
out = self.io.getoutput()
return change, out
def test_identical(self):
change, out = self._show()
assert not change
assert out == ""
def test_string_fixed_field_change(self):
self.b.title = "x"
change, out = self._show()
assert change
assert "title" in out
def test_int_fixed_field_change(self):
self.b.track = 9
change, out = self._show()
assert change
assert "track" in out
def test_floats_close_to_identical(self):
self.a.length = 1.00001
self.b.length = 1.00005
change, out = self._show()
assert not change
assert out == ""
def test_floats_different(self):
self.a.length = 1.00001
self.b.length = 2.00001
change, out = self._show()
assert change
assert "length" in out
def test_both_values_shown(self):
self.a.title = "foo"
self.b.title = "bar"
change, out = self._show()
assert "foo" in out
assert "bar" in out
class PathFormatTest(unittest.TestCase):
def test_custom_paths_prepend(self):
default_formats = ui.get_path_formats()
config["paths"] = {"foo": "bar"}
pf = ui.get_path_formats()
key, tmpl = pf[0]
assert key == "foo"
assert tmpl.original == "bar"
assert pf[1:] == default_formats
@_common.slow_test()
class PluginTest(TestPluginTestCase):
def test_plugin_command_from_pluginpath(self):
self.run_command("test", lib=None)
class CommonOptionsParserCliTest(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 == "xxx/yyy\n"
output = self.run_with_output("ls", "-a", "-p")
assert output == "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
output = self.run_with_output("help", "list")
assert "Usage:" in output
with pytest.raises(ui.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 EncodingTest(unittest.TestCase):
"""Tests for the `terminal_encoding` config option and our
`_in_encoding` and `_out_encoding` utility functions.
"""
def out_encoding_overridden(self):
config["terminal_encoding"] = "fake_encoding"
assert ui._out_encoding() == "fake_encoding"
def in_encoding_overridden(self):
config["terminal_encoding"] = "fake_encoding"
assert ui._in_encoding() == "fake_encoding"
def out_encoding_default_utf8(self):
with patch("sys.stdout") as stdout:
stdout.encoding = None
assert ui._out_encoding() == "utf-8"
def in_encoding_default_utf8(self):
with patch("sys.stdin") as stdin:
stdin.encoding = None
assert ui._in_encoding() == "utf-8"