diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f03ac29..a5f4885 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,9 @@ jobs: - name: apt update run: sudo apt-get update + - name: install APT dependencies + run: + sudo apt-get install fonts-roboto - name: update PIP run: python -m pip install --upgrade pip diff --git a/brother_ql_web/__main__.py b/brother_ql_web/__main__.py index 3487749..b83aa34 100644 --- a/brother_ql_web/__main__.py +++ b/brother_ql_web/__main__.py @@ -6,7 +6,7 @@ def main(): cli_parameters = cli.get_parameters() configuration = Configuration.from_json(cli_parameters.configuration) - configuration = cli.update_configuration_from_parameters( + cli.update_configuration_from_parameters( configuration=configuration, parameters=cli_parameters ) logging.basicConfig(level=configuration.server.log_level) diff --git a/brother_ql_web/cli.py b/brother_ql_web/cli.py index c1b2372..78e6ce5 100644 --- a/brother_ql_web/cli.py +++ b/brother_ql_web/cli.py @@ -96,13 +96,14 @@ def _choose_default_font(fonts: dict, configuration: Configuration) -> None: def update_configuration_from_parameters( parameters: Namespace, configuration: Configuration -) -> Configuration: +): # Server configuration. if parameters.port: configuration.server.port = parameters.port if parameters.log_level: # `log_level` will be numeric if parsed from argv, so we enforce the name here. - configuration.server.log_level = logging.getLevelName(parameters.log_level) + level = parameters.log_level + configuration.server.log_level = logging.getLevelName(level) if isinstance(level, int) else level if parameters.font_folder: configuration.server.additional_font_folder = parameters.font_folder @@ -121,7 +122,7 @@ def update_configuration_from_parameters( # Configuration issues. if configuration.label.default_size not in label_sizes: raise InvalidLabelSize( - "Invalid default label size. Please choose one of the following:\n:" + "Invalid default label size. Please choose one of the following:\n" + " ".join(label_sizes) ) @@ -135,5 +136,3 @@ def update_configuration_from_parameters( # Set font data. _choose_default_font(fonts=fonts, configuration=configuration) - - return configuration diff --git a/brother_ql_web/configuration.py b/brother_ql_web/configuration.py index cdf3423..f704e2b 100644 --- a/brother_ql_web/configuration.py +++ b/brother_ql_web/configuration.py @@ -22,8 +22,13 @@ def from_json(cls, json_file: str): name: str = field.name field_type: str = cast(str, field.type) field_class = global_variables[field_type] - kwargs_inner = parsed.pop(name) - instance = field_class(**kwargs_inner) + kwargs_inner = parsed.pop(name, None) + if name == "printer" and not kwargs_inner: + raise ValueError("Printer configuration missing") + if not kwargs_inner: + instance = field_class() + else: + instance = field_class(**kwargs_inner) kwargs[name] = instance if parsed: raise ValueError(f"Unknown configuration values: {parsed}") @@ -51,7 +56,7 @@ class PrinterConfiguration: printer: str -@dataclass +@dataclass(frozen=True) class Font: family: str style: str diff --git a/tests/__init__.py b/tests/__init__.py index 8fdcc71..f3b04ef 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,9 @@ import logging -from unittest import TestCase # noqa: F401 +from functools import cached_property +from pathlib import Path +from unittest import TestCase as _TestCase + +from brother_ql_web.configuration import Configuration def patch_deprecation_warning(): @@ -22,3 +26,13 @@ def warn(message, *args, **kwargs): patch_deprecation_warning() + + +class TestCase(_TestCase): + @cached_property + def example_configuration_path(self) -> str: + return str(Path(__file__).parent.parent / "config.example.json") + + @property + def example_configuration(self) -> Configuration: + return Configuration.from_json(self.example_configuration_path) diff --git a/tests/test_cli.py b/tests/test_cli.py index af830c6..7b9365e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,11 @@ +from io import StringIO +from contextlib import redirect_stderr +from unittest import mock + from tests import TestCase # Silence useless deprecation warning. from brother_ql_web import cli +from brother_ql_web.configuration import Configuration, Font class LogLevelTypeTestCase(TestCase): @@ -16,12 +21,383 @@ def test_invalid(self): class GetParametersTestCase(TestCase): - pass + def test_get_parameters(self): + with mock.patch( + "sys.argv", + [ + "brother_ql_web/__main__.py", + "--configuration", + self.example_configuration_path, + "--log-level", + "INFO", + "/dev/printer1", + ], + ): + parameters = cli.get_parameters() + + self.assertEqual( + cli.Namespace( + port=False, + log_level=20, + font_folder=False, + default_label_size=False, + default_orientation=False, + model=False, + printer="/dev/printer1", + configuration=self.example_configuration_path, + ), + parameters, + ) class ChooseDefaultFontTestCase(TestCase): - pass + FONTS = { + "family1": { + "Regular": "family1/regular.otf", + "Italic": "family1/italic.otf", + }, + "family2": { + "Regular": "family2/regular.ttf", + "Bold": "family2/bold.ttf", + }, + "family 3": {"Italic": "family3/italic.otf"}, + } + + def assert_is_valid_font(self, font): + self.assertIn(font.family, self.FONTS) + self.assertIn(font.style, self.FONTS[font.family]) + + def test_invalid_only(self): + configuration = self.example_configuration + invalid_font = Font(family="invalid", style="Regular") + configuration.label.default_fonts = [invalid_font] + + stderr = StringIO() + with redirect_stderr(stderr): + self.assertIsNone(configuration.label.default_font) + cli._choose_default_font(fonts=self.FONTS, configuration=configuration) + self.assertIsNotNone(configuration.label.default_font) + font = configuration.label.default_font + self.assertNotEqual(invalid_font, font) + self.assert_is_valid_font(font) + + stderr = stderr.getvalue() + self.assertEqual( + ( + "Could not find any of the default fonts. Choosing a random one.\n" + f"The default font is now set to: {font}\n" + ), + stderr, + ) + + def test_no_font_given(self): + configuration = self.example_configuration + configuration.label.default_fonts = [] + + # Retrieving only two fonts proved to be rather unstable, as we would often + # choose the same font both times, thus the test would not be representative. + # By doing ten iterations and assuring that at least three different fonts have + # been retrieved, we test the same, but in a more stable fashion. Evaluations + # have shown the we usually get 3-5 different fonts out of ten samples. + stderr = StringIO() + fonts = [] + with redirect_stderr(stderr): + self.assertIsNone(configuration.label.default_font) + + for _ in range(10): + cli._choose_default_font(fonts=self.FONTS, configuration=configuration) + self.assertIsNotNone(configuration.label.default_font) + font = configuration.label.default_font + self.assert_is_valid_font(font) + fonts.append(font) + + self.assertGreaterEqual(len(set(fonts)), 3, fonts) + + stderr = stderr.getvalue() + self.assertEqual( + "".join( + ( + "Could not find any of the default fonts. Choosing a random one.\n" + f"The default font is now set to: {font}\n" + ) + for font in fonts + ), + stderr, + ) + + def test_first_valid_font_chosen(self): + configuration = self.example_configuration + configuration.label.default_fonts = [ + Font(family="invalid", style="Regular"), + Font(family="family2", style="Bold"), + Font(family="family1", style="Regular"), + ] + + stderr = StringIO() + with redirect_stderr(stderr): + self.assertIsNone(configuration.label.default_font) + cli._choose_default_font(fonts=self.FONTS, configuration=configuration) + self.assertIsNotNone(configuration.label.default_font) + font = configuration.label.default_font + self.assert_is_valid_font(font) + self.assertEqual(Font(family="family2", style="Bold"), font) + + stderr = stderr.getvalue() + self.assertEqual("", stderr) class UpdateConfigurationFromParametersTestCase(TestCase): - pass + @staticmethod + def dummy_choose_default_font( + fonts: dict, configuration: Configuration + ) -> Configuration: + configuration.label.default_font = Font(family="My Family", style="Bold") + return configuration + + @property + def expected_configuration(self) -> Configuration: + configuration = self.example_configuration + configuration.label.default_font = Font(family="My Family", style="Bold") + return configuration + + def setUp(self): + super().setUp() + + choose_patcher = mock.patch.object( + cli, "_choose_default_font", side_effect=self.dummy_choose_default_font + ) + self.choose_mock = choose_patcher.start() + self.addCleanup(choose_patcher.stop) + + def test_no_overwrites(self): + configuration = self.example_configuration + parameters = cli.Namespace( + port=False, + log_level=False, + font_folder=False, + default_label_size=False, + default_orientation=False, + model=False, + printer=False, + configuration=self.example_configuration_path, + ) + + cli.update_configuration_from_parameters( + parameters=parameters, configuration=configuration + ) + self.choose_mock.assert_called_once() + + self.assertEqual(self.expected_configuration, configuration) + + def test_invalid_label_size(self): + configuration = self.example_configuration + parameters = cli.Namespace( + port=False, + log_level=False, + font_folder=False, + default_label_size=False, + default_orientation=False, + model=False, + printer=False, + configuration=self.example_configuration_path, + ) + configuration.label.default_size = "dummy" + + # Do not assert the label sizes, but make sure that the value list starts with a number. + with self.assertRaisesRegex( + cli.InvalidLabelSize, + "^Invalid default label size\. Please choose one of the following:\\n\d.+$", + ): + cli.update_configuration_from_parameters( + parameters=parameters, configuration=configuration + ) + + def test_no_fonts_found(self): + configuration = self.example_configuration + parameters = cli.Namespace( + port=False, + log_level=False, + font_folder=False, + default_label_size=False, + default_orientation=False, + model=False, + printer=False, + configuration=self.example_configuration_path, + ) + + with mock.patch.object(cli, "collect_fonts", return_value=[]) as collect_mock: + with self.assertRaisesRegex( + cli.NoFontFound, + '^Not a single font was found on your system. Please install some or use the "--font-folder" argument.$', + ): + cli.update_configuration_from_parameters( + parameters=parameters, configuration=configuration + ) + collect_mock.assert_called_once_with(configuration) + + def test_overwrite_port(self): + configuration = self.example_configuration + parameters = cli.Namespace( + port=1337, + log_level=False, + font_folder=False, + default_label_size=False, + default_orientation=False, + model=False, + printer=False, + configuration=self.example_configuration_path, + ) + + cli.update_configuration_from_parameters( + parameters=parameters, configuration=configuration + ) + self.choose_mock.assert_called_once() + + expected_configuration = self.expected_configuration + expected_configuration.server.port = 1337 + self.assertEqual(expected_configuration, configuration) + + def test_overwrite_log_level(self): + for log_level in [20, "INFO"]: + with self.subTest(log_level=log_level): + self.choose_mock.reset_mock() + + configuration = self.example_configuration + parameters = cli.Namespace( + port=False, + log_level=log_level, + font_folder=False, + default_label_size=False, + default_orientation=False, + model=False, + printer=False, + configuration=self.example_configuration_path, + ) + + cli.update_configuration_from_parameters( + parameters=parameters, configuration=configuration + ) + self.choose_mock.assert_called_once() + + expected_configuration = self.expected_configuration + expected_configuration.server.log_level = "INFO" + self.assertEqual(expected_configuration, configuration) + + def test_overwrite_font_folder(self): + configuration = self.example_configuration + parameters = cli.Namespace( + port=False, + log_level=False, + font_folder="/path/to/fonts", + default_label_size=False, + default_orientation=False, + model=False, + printer=False, + configuration=self.example_configuration_path, + ) + + dummy_fonts = {"DejaVu Serif": {"Book": "dummy"}} + with mock.patch( + "brother_ql_web.utils.get_fonts", return_value=dummy_fonts + ) as fonts_mock: + cli.update_configuration_from_parameters( + parameters=parameters, configuration=configuration + ) + self.choose_mock.assert_called_once() + fonts_mock.assert_has_calls( + [mock.call(), mock.call("/path/to/fonts")], + any_order=False, + ) + self.assertEqual(2, fonts_mock.call_count, fonts_mock.call_args_list) + + expected_configuration = self.expected_configuration + expected_configuration.server.additional_font_folder = "/path/to/fonts" + self.assertEqual(expected_configuration, configuration) + + def test_overwrite_printer(self): + configuration = self.example_configuration + parameters = cli.Namespace( + port=False, + log_level=False, + font_folder=False, + default_label_size=False, + default_orientation=False, + model=False, + printer="/dev/printer42", + configuration=self.example_configuration_path, + ) + + cli.update_configuration_from_parameters( + parameters=parameters, configuration=configuration + ) + self.choose_mock.assert_called_once() + + expected_configuration = self.expected_configuration + expected_configuration.printer.printer = "/dev/printer42" + self.assertEqual(expected_configuration, configuration) + + def test_overwrite_model(self): + configuration = self.example_configuration + parameters = cli.Namespace( + port=False, + log_level=False, + font_folder=False, + default_label_size=False, + default_orientation=False, + model="QL-800", + printer=False, + configuration=self.example_configuration_path, + ) + + cli.update_configuration_from_parameters( + parameters=parameters, configuration=configuration + ) + self.choose_mock.assert_called_once() + + expected_configuration = self.expected_configuration + expected_configuration.printer.model = "QL-800" + self.assertEqual(expected_configuration, configuration) + + def test_overwrite_default_label_size(self): + configuration = self.example_configuration + parameters = cli.Namespace( + port=False, + log_level=False, + font_folder=False, + default_label_size="38", + default_orientation=False, + model=False, + printer=False, + configuration=self.example_configuration_path, + ) + + cli.update_configuration_from_parameters( + parameters=parameters, configuration=configuration + ) + self.choose_mock.assert_called_once() + + expected_configuration = self.expected_configuration + expected_configuration.label.default_size = "38" + self.assertEqual(expected_configuration, configuration) + + def test_overwrite_default_orientation(self): + configuration = self.example_configuration + parameters = cli.Namespace( + port=False, + log_level=False, + font_folder=False, + default_label_size=False, + default_orientation="my_orientation", + model=False, + printer=False, + configuration=self.example_configuration_path, + ) + + cli.update_configuration_from_parameters( + parameters=parameters, configuration=configuration + ) + self.choose_mock.assert_called_once() + + expected_configuration = self.expected_configuration + expected_configuration.label.default_orientation = "my_orientation" + self.assertEqual(expected_configuration, configuration) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 420ba88..bf15a60 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1,12 +1,158 @@ -from unittest import TestCase +import json +from tempfile import NamedTemporaryFile + +from brother_ql_web.configuration import ( + Configuration, + Font, + LabelConfiguration, + PrinterConfiguration, + ServerConfiguration, + WebsiteConfiguration, +) + +from tests import TestCase + + +CUSTOM_CONFIGURATION = """ +{ + "server": { + "port": 1337, + "host": "test.local", + "log_level": "ERROR", + "additional_font_folder": "" + }, + "printer": { + "model": "QL-800", + "printer": "file:///dev/usb/lp1" + }, + "label": { + "default_size": "62", + "default_orientation": "standard", + "default_font_size": 42, + "default_fonts": [ + { + "family": "Minion Pro", + "style": "Semibold" + }, + { + "family": "Linux Libertine", + "style": "Regular" + }, + { + "family": "DejaVu Serif", + "style": "Book" + } + ], + "default_font": null + }, + "website": { + "html_title": "Label Designer", + "page_title": "Brother QL Label Designer", + "page_headline": "Design your label and print it!" + } +} +""" class ConfigurationTestCase(TestCase): - pass + @property + def example_json(self) -> dict: + with open(self.example_configuration_path) as fd: + return json.load(fd) + + def test_from_json(self): + with NamedTemporaryFile(suffix=".json", mode="w+t") as json_file: + json_file.write(CUSTOM_CONFIGURATION) + json_file.seek(0) + + configuration = Configuration.from_json(json_file.name) + self.assertEqual( + ServerConfiguration( + port=1337, + host="test.local", + log_level="ERROR", + additional_font_folder="", + ), + configuration.server, + ) + self.assertEqual( + PrinterConfiguration(model="QL-800", printer="file:///dev/usb/lp1"), + configuration.printer, + ) + self.assertEqual( + LabelConfiguration( + default_size="62", + default_orientation="standard", + default_font_size=42, + default_fonts=[ + Font(family="Minion Pro", style="Semibold"), + Font(family="Linux Libertine", style="Regular"), + Font(family="DejaVu Serif", style="Book"), + ], + default_font=None, + ), + configuration.label, + ) + self.assertEqual( + WebsiteConfiguration( + html_title="Label Designer", + page_title="Brother QL Label Designer", + page_headline="Design your label and print it!", + ), + configuration.website, + ) + + def test_from_json__too_many_keys(self): + with NamedTemporaryFile(suffix=".json", mode="w+t") as json_file: + data = self.example_json + data["key"] = {"value": 42} + json_file.write(json.dumps(data)) + json_file.seek(0) + + with self.assertRaisesRegex( + ValueError, + r"^Unknown configuration values: \{'key': \{'value': 42\}\}$", + ): + Configuration.from_json(json_file.name) + + def test_from_json__missing_server_key(self): + with NamedTemporaryFile(suffix=".json", mode="w+t") as json_file: + data = self.example_json + del data["server"] + json_file.write(json.dumps(data)) + json_file.seek(0) + + configuration = Configuration.from_json(json_file.name) + self.assertEqual(ServerConfiguration(), configuration.server) + + def test_from_json__missing_printer_key(self): + with NamedTemporaryFile(suffix=".json", mode="w+t") as json_file: + data = self.example_json + del data["printer"] + json_file.write(json.dumps(data)) + json_file.seek(0) + + with self.assertRaisesRegex(ValueError, r"^Printer configuration missing$"): + Configuration.from_json(json_file.name) + + def test_to_json(self): + with NamedTemporaryFile(suffix=".json", mode="w+t") as json_file: + json_file.write(CUSTOM_CONFIGURATION) + json_file.seek(0) + + configuration = Configuration.from_json(json_file.name) + self.assertEqual( + CUSTOM_CONFIGURATION.strip(), configuration.to_json().strip() + ) class ServerConfigurationTestCase(TestCase): - pass + def test_is_in_debug_mode(self): + self.assertTrue(ServerConfiguration(log_level="DEBUG").is_in_debug_mode) + + for level in ["INFO", "WARNING", "ERROR"]: + with self.subTest(level=level): + self.assertFalse(ServerConfiguration(log_level=level).is_in_debug_mode) class PrinterConfigurationTestCase(TestCase): @@ -18,7 +164,19 @@ class FontTestCase(TestCase): class LabelConfigurationTestCase(TestCase): - pass + def test_post_init(self): + default_fonts = [ + Font(family="Font family", style="Regular"), + {"family": "Another font family", "style": "Bold"}, + ] + configuration = LabelConfiguration(default_fonts=default_fonts) + self.assertEqual( + [ + Font(family="Font family", style="Regular"), + Font(family="Another font family", style="Bold"), + ], + configuration.default_fonts, + ) class WebsiteConfigurationTestCase(TestCase): diff --git a/tests/test_font_helpers.py b/tests/test_font_helpers.py index 5b81cd6..3181f8c 100644 --- a/tests/test_font_helpers.py +++ b/tests/test_font_helpers.py @@ -1,5 +1,83 @@ from unittest import TestCase +from brother_ql_web.font_helpers import get_fonts + class GetFontsTestCase(TestCase): - pass + # Reference: https://packages.ubuntu.com/lunar/all/fonts-roboto-unhinted/filelist + ROBOTO_FILES = { + "Roboto": { + "Thin": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Thin.ttf", + "Medium Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-MediumItalic.ttf", + "Thin Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-ThinItalic.ttf", + "Light Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-LightItalic.ttf", + "Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Italic.ttf", + "Black Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-BlackItalic.ttf", + "Medium": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Medium.ttf", + "Bold": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Bold.ttf", + "Black": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Black.ttf", + "Light": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Light.ttf", + "Regular": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Regular.ttf", + "Bold Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-BoldItalic.ttf", + }, + "Roboto Black": { + "Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-BlackItalic.ttf", + "Regular": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Black.ttf", + }, + "Roboto Condensed": { + "Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-Italic.ttf", + "Bold": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-Bold.ttf", + "Medium": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-Medium.ttf", + "Bold Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-BoldItalic.ttf", + "Light Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-LightItalic.ttf", + "Light": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-Light.ttf", + "Medium Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-MediumItalic.ttf", + "Regular": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-Regular.ttf", + }, + "Roboto Condensed Light": { + "Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-LightItalic.ttf", + "Regular": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-Light.ttf", + }, + "Roboto Condensed Medium": { + "Regular": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-Medium.ttf", + "Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoCondensed-MediumItalic.ttf", + }, + "Roboto Light": { + "Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-LightItalic.ttf", + "Regular": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Light.ttf", + }, + "Roboto Medium": { + "Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-MediumItalic.ttf", + "Regular": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Medium.ttf", + }, + "Roboto Thin": { + "Regular": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-Thin.ttf", + "Italic": "/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF/Roboto-ThinItalic.ttf", + }, + } + + def assert_font_dictionary_subset(self, expected, actual, not_in_name: str = ""): + for font_name, font_styles in expected.items(): + self.assertIn(font_name, actual) + actual_styles = actual[font_name] + with self.subTest(font_name=font_name): + for font_style, path in font_styles.items(): + self.assertIn(font_style, actual_styles) + self.assertEqual(path, actual_styles[font_style], font_style) + if not_in_name: + self.assertNotIn(not_in_name, font_name) + + def test_get_all(self): + fonts = get_fonts() + self.assert_font_dictionary_subset(expected=self.ROBOTO_FILES, actual=fonts) + + def test_get_from_folder(self): + fonts = get_fonts(folder="/usr/share/fonts/truetype/roboto/unhinted/RobotoTTF") + expected = { + key: value + for key, value in self.ROBOTO_FILES.items() + if "Condensed" not in key + } + self.assert_font_dictionary_subset( + expected=expected, actual=fonts, not_in_name="Condensed" + ) diff --git a/tests/test_main.py b/tests/test_main.py index 67c2e31..ba10757 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,50 @@ -from unittest import TestCase +from unittest import mock +from brother_ql_web.__main__ import main +from brother_ql_web.configuration import Font -class MainTestCase(TestCase): +from tests import TestCase + + +class Backend: pass + + +class MainTestCase(TestCase): + def test_main(self): + fonts = { + "DejaVu Serif": {"Book": "dummy", "Regular": "dummy2"}, + } + label_sizes = [("62", "62mm labels"), "42", "42mm labels"] + + with mock.patch( + "sys.argv", + [ + "brother_ql_web/__main__", + "--configuration", + self.example_configuration_path, + ], + ), mock.patch("logging.basicConfig") as basic_config_mock, mock.patch( + "brother_ql_web.utils.collect_fonts", return_value=fonts + ) as fonts_mock, mock.patch( + "brother_ql_web.utils.get_label_sizes", return_value=label_sizes + ) as labels_mock, mock.patch( + "brother_ql_web.utils.get_backend_class", return_value=Backend + ) as backend_mock, mock.patch( + "brother_ql_web.web.main" + ) as main_mock: + main() + + configuration = self.example_configuration + configuration.label.default_font = Font(family="DejaVu Serif", style="Book") + + basic_config_mock.assert_called_once_with(level="WARNING") + main_mock.assert_called_once_with( + configuration=configuration, + fonts=fonts, + label_sizes=label_sizes, + backend_class=Backend, + ) + fonts_mock.assert_called_once_with(configuration) + labels_mock.assert_called_once_with() + backend_mock.assert_called_once_with(configuration) diff --git a/tests/test_utils.py b/tests/test_utils.py index 20ce847..7ce0530 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,13 +1,78 @@ -from unittest import TestCase +from unittest import mock + +from brother_ql_web import utils + +from tests import TestCase class CollectFontsTestCase(TestCase): - pass + def test_without_folder(self): + dummy_fonts = { + "DejaVu Serif": {"Book": "dummy", "Regular": "dummy2"}, + "Font": {"Style": "path"}, + } + configuration = self.example_configuration + with mock.patch.object( + utils, "get_fonts", return_value=dummy_fonts + ) as get_mock: + fonts = utils.collect_fonts(configuration) + get_mock.assert_called_once_with() + self.assertEqual(dummy_fonts, fonts) + + def test_with_folder(self): + dummy_fonts = { + "DejaVu Serif": {"Book": "dummy", "Regular": "dummy2"}, + "Font": {"Style": "path"}, + } + folder_fonts = { + "Font": {"Style": "/path", "Another Style": "/path2"}, + "Symbol": {"Regular": "/path3"}, + } + + def get_fonts(directory: None | str = None): + return folder_fonts if directory else dummy_fonts + + configuration = self.example_configuration + configuration.server.additional_font_folder = "/another_path" + with mock.patch.object(utils, "get_fonts", side_effect=get_fonts) as get_mock: + fonts = utils.collect_fonts(configuration) + + get_mock.assert_has_calls( + [mock.call(), mock.call("/another_path")], any_order=False + ) + self.assertEqual(2, get_mock.call_count, get_mock.call_args_list) + + self.assertEqual( + { + "DejaVu Serif": {"Book": "dummy", "Regular": "dummy2"}, + "Font": {"Style": "/path", "Another Style": "/path2"}, + "Symbol": {"Regular": "/path3"}, + }, + fonts, + ) class GetLabelSizesTestCase(TestCase): - pass + def test_get_label_sizes(self): + sizes = utils.get_label_sizes() + self.assertIn(("38", "38mm endless"), sizes) + self.assertIn(("62x29", "62mm x 29mm die-cut"), sizes) class GetBackendClassTestCase(TestCase): - pass + def test_unknown_backend(self): + configuration = self.example_configuration + configuration.printer.printer = "dummy" + + with self.assertRaisesRegex( + utils.BackendGuessingError, + r"^Couln't guess the backend to use from the printer string descriptor$", + ): + backend = utils.get_backend_class(configuration) + + def test_known_backend(self): + from brother_ql.backends.linux_kernel import BrotherQLBackendLinuxKernel + + configuration = self.example_configuration + backend = utils.get_backend_class(configuration) + self.assertIs(backend, BrotherQLBackendLinuxKernel)