ci(tests): fix broken tests and add type hints (#1062)

* ci: expand pyright ignore rules to vendored and tests

* tests: comment out unused Mocks for further evaluation

* tests: fix broken tests, add type hints

* chore: address type feedback

* chore: remove unused qtbot parameter
This commit is contained in:
Travis Abendshien
2025-08-27 04:33:38 -07:00
committed by GitHub
parent 3a0da4699a
commit 4704b92804
24 changed files with 335 additions and 225 deletions

View File

@@ -1,4 +1,10 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import sys
from collections.abc import Callable, Generator
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import Mock, patch
@@ -26,6 +32,7 @@ def file_mediatypes_library():
status = lib.open_library(Path(""), ":memory:")
assert status.success
assert lib.folder
entry1 = Entry(
folder=lib.folder,
@@ -64,18 +71,19 @@ def library_dir():
@pytest.fixture
def library(request, library_dir: Path):
def library(request, library_dir: Path): # pyright: ignore
# when no param is passed, use the default
library_path = library_dir
if hasattr(request, "param"):
if isinstance(request.param, TemporaryDirectory):
library_path = Path(request.param.name)
library_path = Path(request.param.name) # pyright: ignore[reportArgumentType]
else:
library_path = Path(request.param)
lib = Library()
status = lib.open_library(library_path, ":memory:")
assert status.success
assert lib.folder
tag = Tag(
name="foo",
@@ -133,7 +141,7 @@ def search_library() -> Library:
@pytest.fixture
def entry_min(library):
def entry_min(library: Library):
yield next(library.all_entries())
@@ -143,7 +151,7 @@ def entry_full(library: Library):
@pytest.fixture
def qt_driver(qtbot, library, library_dir: Path):
def qt_driver(library: Library, library_dir: Path):
class Args:
settings_file = library_dir / "settings.toml"
cache_file = library_dir / "tagstudio.ini"
@@ -151,31 +159,26 @@ def qt_driver(qtbot, library, library_dir: Path):
ci = True
with patch("tagstudio.qt.ts_qt.Consumer"), patch("tagstudio.qt.ts_qt.CustomRunnable"):
driver = QtDriver(Args())
driver = QtDriver(Args()) # pyright: ignore[reportArgumentType]
driver.app = Mock()
driver.main_window = Mock()
driver.main_window.preview_panel = Mock()
driver.main_window.thumb_grid = Mock()
driver.main_window.thumb_size = 128
driver.item_thumbs = []
driver.main_window.menu_bar.autofill_action = Mock()
driver.copy_buffer = {"fields": [], "tags": []}
driver.main_window.menu_bar.copy_fields_action = Mock()
driver.main_window.menu_bar.paste_fields_action = Mock()
driver.lib = library
# TODO - downsize this method and use it
# driver.start()
driver.frame_content = list(library.all_entries())
driver.frame_content = [e.id for e in library.all_entries()]
yield driver
@pytest.fixture
def generate_tag():
def inner(name, **kwargs):
def generate_tag() -> Generator[Callable[..., Tag]]:
def inner(name: str, **kwargs) -> Tag: # pyright: ignore
params = dict(name=name, color_namespace="tagstudio-standard", color_slug="red") | kwargs
return Tag(**params)
return Tag(**params) # pyright: ignore[reportArgumentType]
yield inner

View File

@@ -1,13 +1,20 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from pathlib import Path
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.utils.dupe_files import DupeRegistry
CWD = Path(__file__).parent
def test_refresh_dupe_files(library):
def test_refresh_dupe_files(library: Library):
library.library_dir = Path("/tmp/")
assert library.folder
entry = Entry(
folder=library.folder,
path=Path("bar/foo.txt"),

View File

@@ -1,7 +1,12 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.modals.folders_to_tags import folders_to_tags
def test_folders_to_tags(library):
def test_folders_to_tags(library: Library):
folders_to_tags(library)
entry = [x for x in library.all_entries(with_joins=True) if "bar.md" in str(x.path)][0]
assert {x.name for x in entry.tags} == {"two", "bar"}

View File

@@ -1,3 +1,7 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from pathlib import Path
from tempfile import TemporaryDirectory
@@ -16,6 +20,7 @@ def test_refresh_missing_files(library: Library):
registry = MissingRegistry(library=library)
# touch the file `one/two/bar.md` but in wrong location to simulate a moved file
assert library.library_dir
(library.library_dir / "bar.md").touch()
# no files actually exist, so it should return all entries

View File

@@ -1,9 +1,14 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from tagstudio.core.enums import LibraryPrefs
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.utils.refresh_dir import RefreshDirTracker
CWD = Path(__file__).parent
@@ -11,7 +16,8 @@ CWD = Path(__file__).parent
@pytest.mark.parametrize("exclude_mode", [True, False])
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
def test_refresh_new_files(library, exclude_mode):
def test_refresh_new_files(library: Library, exclude_mode: bool):
assert library.library_dir
# Given
library.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, exclude_mode)
library.set_prefs(LibraryPrefs.EXTENSION_LIST, [".md"])

View File

@@ -1,37 +0,0 @@
# import shutil
# from pathlib import Path
# from tempfile import TemporaryDirectory
# import pytest
# from tagstudio.core.enums import MacroID
# from tagstudio.core.library.alchemy.fields import _FieldID
# @pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
def test_sidecar_macro(qt_driver, library, cwd, entry_full):
# TODO: Rework and finalize sidecar loading + macro systems.
pass
# entry_full.path = Path("newgrounds/foo.txt")
# fixture = cwd / "fixtures/sidecar_newgrounds.json"
# dst = library.library_dir / "newgrounds" / (entry_full.path.name + ".json")
# dst.parent.mkdir()
# shutil.copy(fixture, dst)
# qt_driver.frame_content = [entry_full]
# qt_driver.run_macro(MacroID.SIDECAR, entry_full.id)
# entry = library.get_entry_full(entry_full.id)
# new_fields = (
# (_FieldID.DESCRIPTION.name, "NG description"),
# (_FieldID.ARTIST.name, "NG artist"),
# (_FieldID.SOURCE.name, "https://ng.com"),
# )
# found = [(field.type.key, field.value) for field in entry.fields]
# # `new_fields` should be subset of `found`
# for field in new_fields:
# assert field in found, f"Field not found: {field} / {found}"
# expected_tags = {"ng_tag", "ng_tag2"}
# assert {x.name in expected_tags for x in entry.tags}

View File

@@ -1,22 +1,37 @@
from tagstudio.core.library.alchemy.models import Tag
from tagstudio.qt.modals.build_tag import BuildTagPanel
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from collections.abc import Callable
from pytestqt.qtbot import QtBot
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Tag, TagAlias
from tagstudio.qt.modals.build_tag import BuildTagPanel, CustomTableItem
from tagstudio.qt.translations import Translations
def test_build_tag_panel_add_sub_tag_callback(library, generate_tag):
def test_build_tag_panel_add_sub_tag_callback(
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
):
parent = library.add_tag(generate_tag("xxx", id=123))
child = library.add_tag(generate_tag("xx", id=124))
assert child
assert parent
panel: BuildTagPanel = BuildTagPanel(library, child)
qtbot.addWidget(panel)
panel.add_parent_tag_callback(parent.id)
assert len(panel.parent_ids) == 1
def test_build_tag_panel_remove_subtag_callback(library, generate_tag):
def test_build_tag_panel_remove_subtag_callback(
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
):
parent = library.add_tag(generate_tag("xxx", id=123))
child = library.add_tag(generate_tag("xx", id=124))
assert child
@@ -29,6 +44,7 @@ def test_build_tag_panel_remove_subtag_callback(library, generate_tag):
assert child
panel: BuildTagPanel = BuildTagPanel(library, child)
qtbot.addWidget(panel)
panel.remove_parent_tag_callback(parent.id)
@@ -40,31 +56,39 @@ import os
os.environ["QT_QPA_PLATFORM"] = "offscreen"
def test_build_tag_panel_add_alias_callback(library, generate_tag):
def test_build_tag_panel_add_alias_callback(
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
):
tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
panel: BuildTagPanel = BuildTagPanel(library, tag)
qtbot.addWidget(panel)
panel.add_alias_callback()
assert panel.aliases_table.rowCount() == 1
def test_build_tag_panel_remove_alias_callback(library, generate_tag):
tag = library.add_tag(generate_tag("xxx", id=123))
def test_build_tag_panel_remove_alias_callback(
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
):
tag: Tag | None = library.add_tag(generate_tag("xxx", id=123))
assert tag
library.update_tag(tag, [], {"alias", "alias_2"}, {123, 124})
tag = library.get_tag(tag.id)
assert tag
assert "alias" in tag.alias_strings
assert "alias_2" in tag.alias_strings
panel: BuildTagPanel = BuildTagPanel(library, tag)
qtbot.addWidget(panel)
alias = library.get_alias(tag.id, tag.alias_ids[0])
alias: TagAlias | None = library.get_alias(tag.id, tag.alias_ids[0])
assert alias
panel.remove_alias_callback(alias.name, alias.id)
@@ -73,7 +97,9 @@ def test_build_tag_panel_remove_alias_callback(library, generate_tag):
assert alias.name not in panel.alias_names
def test_build_tag_panel_set_parent_tags(library, generate_tag):
def test_build_tag_panel_set_parent_tags(
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
):
parent = library.add_tag(generate_tag("parent", id=123))
child = library.add_tag(generate_tag("child", id=124))
assert parent
@@ -84,30 +110,37 @@ def test_build_tag_panel_set_parent_tags(library, generate_tag):
child = library.get_tag(child.id)
panel: BuildTagPanel = BuildTagPanel(library, child)
qtbot.addWidget(panel)
assert len(panel.parent_ids) == 1
assert panel.parent_tags_scroll_layout.count() == 1
def test_build_tag_panel_add_aliases(library, generate_tag):
tag = library.add_tag(generate_tag("xxx", id=123))
def test_build_tag_panel_add_aliases(
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
):
tag: Tag | None = library.add_tag(generate_tag("xxx", id=123))
assert tag
library.update_tag(tag, [], {"alias", "alias_2"}, {123, 124})
tag = library.get_tag(tag.id)
assert tag
assert "alias" in tag.alias_strings
assert "alias_2" in tag.alias_strings
panel: BuildTagPanel = BuildTagPanel(library, tag)
qtbot.addWidget(panel)
widget = panel.aliases_table.cellWidget(0, 1)
assert isinstance(widget, CustomTableItem)
alias_names: set[str] = set()
alias_names.add(widget.text())
widget = panel.aliases_table.cellWidget(1, 1)
assert isinstance(widget, CustomTableItem)
alias_names.add(widget.text())
assert "alias" in alias_names
@@ -123,35 +156,41 @@ def test_build_tag_panel_add_aliases(library, generate_tag):
assert len(panel.alias_names) == 2
def test_build_tag_panel_set_aliases(library, generate_tag):
tag = library.add_tag(generate_tag("xxx", id=123))
def test_build_tag_panel_set_aliases(
qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]
):
tag: Tag | None = library.add_tag(generate_tag("xxx", id=123))
assert tag
library.update_tag(tag, [], {"alias"}, {123})
tag = library.get_tag(tag.id)
assert tag
assert len(tag.alias_ids) == 1
panel: BuildTagPanel = BuildTagPanel(library, tag)
qtbot.addWidget(panel)
assert panel.aliases_table.rowCount() == 1
assert len(panel.alias_names) == 1
assert len(panel.alias_ids) == 1
def test_build_tag_panel_set_tag(library, generate_tag):
def test_build_tag_panel_set_tag(qtbot: QtBot, library: Library, generate_tag: Callable[..., Tag]):
tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
panel: BuildTagPanel = BuildTagPanel(library, tag)
qtbot.addWidget(panel)
assert panel.tag
assert panel.tag.name == "xxx"
def test_build_tag_panel_build_tag(library):
def test_build_tag_panel_build_tag(qtbot: QtBot, library: Library):
panel: BuildTagPanel = BuildTagPanel(library)
qtbot.addWidget(panel)
tag: Tag = panel.build_tag()

View File

@@ -1,7 +1,15 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry, Tag
from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel
from tagstudio.qt.ts_qt import QtDriver
def test_update_selection_empty(qt_driver, library):
def test_update_selection_empty(qt_driver: QtDriver, library: Library):
panel = PreviewPanel(library, qt_driver)
# Clear the library selection (selecting 1 then unselecting 1)
@@ -14,7 +22,7 @@ def test_update_selection_empty(qt_driver, library):
assert container.isHidden()
def test_update_selection_single(qt_driver, library, entry_full):
def test_update_selection_single(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
# Select the single entry
@@ -26,7 +34,7 @@ def test_update_selection_single(qt_driver, library, entry_full):
assert not container.isHidden()
def test_update_selection_multiple(qt_driver, library):
def test_update_selection_multiple(qt_driver: QtDriver, library: Library):
# TODO: Implement mixed field editing. Currently these containers will be hidden,
# same as the empty selection behavior.
panel = PreviewPanel(library, qt_driver)
@@ -41,7 +49,7 @@ def test_update_selection_multiple(qt_driver, library):
assert container.isHidden()
def test_add_tag_to_selection_single(qt_driver, library, entry_full):
def test_add_tag_to_selection_single(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
assert {t.id for t in entry_full.tags} == {1000}
@@ -54,11 +62,11 @@ def test_add_tag_to_selection_single(qt_driver, library, entry_full):
panel.field_containers_widget.add_tags_to_selected(2000)
# Then reload entry
refreshed_entry = next(library.all_entries(with_joins=True))
refreshed_entry: Entry = next(library.all_entries(with_joins=True))
assert {t.id for t in refreshed_entry.tags} == {1000, 2000}
def test_add_same_tag_to_selection_single(qt_driver, library, entry_full):
def test_add_same_tag_to_selection_single(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
assert {t.id for t in entry_full.tags} == {1000}
@@ -75,7 +83,7 @@ def test_add_same_tag_to_selection_single(qt_driver, library, entry_full):
assert {t.id for t in refreshed_entry.tags} == {1000}
def test_add_tag_to_selection_multiple(qt_driver, library):
def test_add_tag_to_selection_multiple(qt_driver: QtDriver, library: Library):
panel = PreviewPanel(library, qt_driver)
all_entries = library.all_entries(with_joins=True)
@@ -102,8 +110,8 @@ def test_add_tag_to_selection_multiple(qt_driver, library):
# Then reload all entries and recheck the presence of tag 1000
refreshed_entries = library.all_entries(with_joins=True)
tag_present_on_some: bool = False
tag_absent_on_some: bool = False
tag_present_on_some = False
tag_absent_on_some = False
for e in refreshed_entries:
if 1000 in [t.id for t in e.tags]:
@@ -115,7 +123,7 @@ def test_add_tag_to_selection_multiple(qt_driver, library):
assert not tag_absent_on_some
def test_meta_tag_category(qt_driver, library, entry_full):
def test_meta_tag_category(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
# Ensure the Favorite tag is on entry_full
@@ -131,20 +139,25 @@ def test_meta_tag_category(qt_driver, library, entry_full):
match i:
case 0:
# Check if the container is the Meta Tags category
assert container.title == f"<h4>{library.get_tag(2).name}</h4>"
tag: Tag | None = library.get_tag(2)
assert tag
assert container.title == f"<h4>{tag.name}</h4>"
case 1:
# Check if the container is the Tags category
assert container.title == "<h4>Tags</h4>"
case 2:
# Make sure the container isn't a duplicate Tags category
assert container.title != "<h4>Tags</h4>"
case _:
pass
def test_custom_tag_category(qt_driver, library, entry_full):
def test_custom_tag_category(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
# Set tag 1000 (foo) as a category
tag = library.get_tag(1000)
tag: Tag | None = library.get_tag(1000)
assert tag
tag.is_category = True
library.update_tag(
tag,
@@ -163,10 +176,14 @@ def test_custom_tag_category(qt_driver, library, entry_full):
match i:
case 0:
# Check if the container is the Meta Tags category
assert container.title == f"<h4>{library.get_tag(2).name}</h4>"
tag_2: Tag | None = library.get_tag(2)
assert tag_2
assert container.title == f"<h4>{tag_2.name}</h4>"
case 1:
# Check if the container is the custom "foo" category
assert container.title == f"<h4>{tag.name}</h4>"
case 2:
# Make sure the container isn't a plain Tags category
assert container.title != "<h4>Tags</h4>"
case _:
pass

View File

@@ -1,4 +1,10 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import os
from collections.abc import Callable
from pathlib import Path
from unittest.mock import patch
@@ -53,7 +59,10 @@ def test_filepath_setting(qtbot: QtBot, qt_driver: QtDriver, filepath_option: Sh
],
)
def test_file_path_display(
qt_driver: QtDriver, library: Library, filepath_option: ShowFilepathOption, expected_path
qt_driver: QtDriver,
library: Library,
filepath_option: ShowFilepathOption,
expected_path: Callable[[Library], Path],
):
panel = PreviewPanel(library, qt_driver)
@@ -68,12 +77,12 @@ def test_file_path_display(
assert isinstance(entry, Entry)
filename = entry.path
assert library.library_dir is not None
panel._file_attributes_widget.update_stats(filepath=library.library_dir / filename)
panel._file_attributes_widget.update_stats(filepath=library.library_dir / filename) # pyright: ignore[reportPrivateUsage]
# Generate the expected file string.
# This is copied directly from the file_attributes.py file
# can be imported as a function in the future
display_path = expected_path(library)
display_path: Path = expected_path(library)
file_str: str = ""
separator: str = f"<a style='color: #777777'><b>{os.path.sep}</a>" # Gray
for i, part in enumerate(display_path.parts):
@@ -86,7 +95,7 @@ def test_file_path_display(
file_str += f"<b>{'\u200b'.join(part_)}</b>"
# Assert the file path is displayed correctly
assert panel._file_attributes_widget.file_label.text() == file_str
assert panel._file_attributes_widget.file_label.text() == file_str # pyright: ignore[reportPrivateUsage]
@pytest.mark.parametrize(
@@ -107,7 +116,10 @@ def test_file_path_display(
],
)
def test_title_update(
qt_driver: QtDriver, filepath_option: ShowFilepathOption, expected_title, library_dir: Path
qt_driver: QtDriver,
filepath_option: ShowFilepathOption,
expected_title: Callable[[Path, str], str],
library_dir: Path,
):
base_title = qt_driver.base_title
@@ -135,7 +147,7 @@ def test_title_update(
qt_driver.main_window.menu_bar.folders_to_tags_action = QAction(menu_bar)
# Trigger the update
qt_driver._init_library(library_dir, open_status)
qt_driver._init_library(library_dir, open_status) # pyright: ignore[reportPrivateUsage]
# Assert the title is updated correctly
qt_driver.main_window.setWindowTitle.assert_called_with(expected_title(library_dir, base_title))
qt_driver.main_window.setWindowTitle.assert_called_with(expected_title(library_dir, base_title)) # pyright: ignore[reportAttributeAccessIssue]

View File

@@ -1,10 +1,15 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import QRect
from PySide6.QtWidgets import QPushButton, QWidget
from tagstudio.qt.flowlayout import FlowLayout
def test_flow_layout_happy_path(qtbot):
def test_flow_layout_happy_path():
class Window(QWidget):
def __init__(self):
super().__init__()
@@ -15,4 +20,4 @@ def test_flow_layout_happy_path(qtbot):
window = Window()
assert window.flow_layout.count()
assert window.flow_layout._do_layout(QRect(0, 0, 0, 0), test_only=False)
assert window.flow_layout._do_layout(QRect(0, 0, 0, 0), test_only=False) # pyright: ignore[reportPrivateUsage]

View File

@@ -1,7 +1,13 @@
from tagstudio.qt.modals.folders_to_tags import generate_preview_data
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
def test_generate_preview_data(library, snapshot):
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.modals.folders_to_tags import BranchData, generate_preview_data
def test_generate_preview_data(library: Library, snapshot: BranchData):
preview = generate_preview_data(library)
assert preview == snapshot

View File

@@ -1,3 +1,8 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from pathlib import Path
from tagstudio.core.global_settings import GlobalSettings, Theme

View File

@@ -1,12 +1,20 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import pytest
from tagstudio.core.library.alchemy.enums import ItemType
from tagstudio.qt.ts_qt import QtDriver
from tagstudio.qt.widgets.item_thumb import BadgeType, ItemThumb
@pytest.mark.parametrize("new_value", (True, False))
def test_badge_visual_state(library, qt_driver, entry_min, new_value):
thumb = ItemThumb(ItemType.ENTRY, qt_driver.lib, qt_driver, (100, 100), 0)
def test_badge_visual_state(qt_driver: QtDriver, entry_min: int, new_value: bool):
thumb = ItemThumb(
ItemType.ENTRY, qt_driver.lib, qt_driver, (100, 100), show_filename_label=False
)
qt_driver.frame_content = [entry_min]
qt_driver.selected = [0]

View File

@@ -1,7 +1,15 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.qt.controller.widgets.preview_panel_controller import PreviewPanel
from tagstudio.qt.ts_qt import QtDriver
def test_update_selection_empty(qt_driver, library):
def test_update_selection_empty(qt_driver: QtDriver, library: Library):
panel = PreviewPanel(library, qt_driver)
# Clear the library selection (selecting 1 then unselecting 1)
@@ -13,7 +21,7 @@ def test_update_selection_empty(qt_driver, library):
assert not panel.add_buttons_enabled
def test_update_selection_single(qt_driver, library, entry_full):
def test_update_selection_single(qt_driver: QtDriver, library: Library, entry_full: Entry):
panel = PreviewPanel(library, qt_driver)
# Select the single entry
@@ -24,7 +32,7 @@ def test_update_selection_single(qt_driver, library, entry_full):
assert panel.add_buttons_enabled
def test_update_selection_multiple(qt_driver, library):
def test_update_selection_multiple(qt_driver: QtDriver, library: Library):
panel = PreviewPanel(library, qt_driver)
# Select the multiple entries

View File

@@ -1,77 +1,19 @@
from typing import TYPE_CHECKING
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.json.library import ItemType
from tagstudio.core.library.alchemy.enums import BrowsingState, ItemType
from tagstudio.qt.ts_qt import QtDriver
from tagstudio.qt.widgets.item_thumb import ItemThumb
if TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
# def test_update_thumbs(qt_driver):
# qt_driver.frame_content = [
# Entry(
# folder=qt_driver.lib.folder,
# path=Path("/tmp/foo"),
# fields=qt_driver.lib.default_fields,
# )
# ]
# qt_driver.item_thumbs = []
# for _ in range(3):
# qt_driver.item_thumbs.append(
# ItemThumb(
# mode=ItemType.ENTRY,
# library=qt_driver.lib,
# driver=qt_driver,
# thumb_size=(100, 100),
# )
# )
# qt_driver.update_thumbs()
# for idx, thumb in enumerate(qt_driver.item_thumbs):
# # only first item is visible
# assert thumb.isVisible() == (idx == 0)
# def test_toggle_item_selection_bridge(qt_driver, entry_min):
# # mock some props since we're not running `start()`
# qt_driver.autofill_action = Mock()
# qt_driver.sort_fields_action = Mock()
# # set the content manually
# qt_driver.frame_content = [entry_min] * 3
# qt_driver.filter.page_size = 3
# qt_driver._init_thumb_grid()
# assert len(qt_driver.item_thumbs) == 3
# # select first item
# qt_driver.toggle_item_selection(0, append=False, bridge=False)
# assert qt_driver.selected == [0]
# # add second item to selection
# qt_driver.toggle_item_selection(1, append=False, bridge=True)
# assert qt_driver.selected == [0, 1]
# # add third item to selection
# qt_driver.toggle_item_selection(2, append=False, bridge=True)
# assert qt_driver.selected == [0, 1, 2]
# # select third item only
# qt_driver.toggle_item_selection(2, append=False, bridge=False)
# assert qt_driver.selected == [2]
# qt_driver.toggle_item_selection(0, append=False, bridge=True)
# assert qt_driver.selected == [0, 1, 2]
def test_browsing_state_update(qt_driver: "QtDriver"):
def test_browsing_state_update(qt_driver: QtDriver):
# Given
for entry in qt_driver.lib.all_entries(with_joins=True):
thumb = ItemThumb(ItemType.ENTRY, qt_driver.lib, qt_driver, (100, 100))
qt_driver.item_thumbs.append(thumb)
qt_driver.frame_content.append(entry)
qt_driver.frame_content.append(entry.id)
# no filter, both items are returned
qt_driver.update_browsing_state()
@@ -82,12 +24,14 @@ def test_browsing_state_update(qt_driver: "QtDriver"):
qt_driver.update_browsing_state(state)
assert len(qt_driver.frame_content) == 1
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
assert entry
assert list(entry.tags)[0].name == "foo"
# When state is not changed, previous one is still applied
qt_driver.update_browsing_state()
assert len(qt_driver.frame_content) == 1
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
assert entry
assert list(entry.tags)[0].name == "foo"
# When state property is changed, previous one is overwritten
@@ -95,10 +39,11 @@ def test_browsing_state_update(qt_driver: "QtDriver"):
qt_driver.update_browsing_state(state)
assert len(qt_driver.frame_content) == 1
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
assert entry
assert list(entry.tags)[0].name == "bar"
def test_close_library(qt_driver):
def test_close_library(qt_driver: QtDriver):
# Given
qt_driver.close_library()

View File

@@ -1,24 +1,34 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from pytestqt.qtbot import QtBot
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Tag
from tagstudio.qt.modals.build_tag import BuildTagPanel
from tagstudio.qt.ts_qt import QtDriver
def test_tag_panel(qtbot, library):
def test_tag_panel(qtbot: QtBot, library: Library):
panel = BuildTagPanel(library)
qtbot.addWidget(panel)
def test_add_tag_callback(qt_driver):
def test_add_tag_callback(qt_driver: QtDriver):
# Given
assert len(qt_driver.lib.tags) == 6
qt_driver.add_tag_action_callback()
# When
assert isinstance(qt_driver.modal.widget, BuildTagPanel)
qt_driver.modal.widget.name_field.setText("xxx")
# qt_driver.modal.widget.color_field.setCurrentIndex(1)
qt_driver.modal.saved.emit()
# Then
tags: set[Tag] = qt_driver.lib.tags
tags: list[Tag] = qt_driver.lib.tags
assert len(tags) == 7
assert "xxx" in {tag.name for tag in tags}

View File

@@ -1,7 +1,15 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from pytestqt.qtbot import QtBot
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.modals.tag_search import TagSearchPanel
def test_update_tags(qtbot, library):
def test_update_tags(qtbot: QtBot, library: Library):
# Given
panel = TagSearchPanel(library)

View File

@@ -1,3 +1,8 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from pathlib import Path
from PySide6.QtCore import QSettings

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
@@ -48,5 +48,5 @@ def test_json_migration():
# List Type
assert modal.check_ext_type()
# No Leading Dot
for ext in modal.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST):
for ext in modal.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST): # pyright: ignore[reportUnknownVariableType]
assert ext[0] != "."

View File

@@ -1,16 +1,28 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from collections.abc import Callable
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
import structlog
from tagstudio.core.enums import DefaultEnum, LibraryPrefs
from tagstudio.core.library.alchemy.enums import BrowsingState
from tagstudio.core.library.alchemy.fields import TextField, _FieldID
from tagstudio.core.library.alchemy.fields import (
TextField,
_FieldID, # pyright: ignore[reportPrivateUsage]
)
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.alchemy.models import Entry, Tag
logger = structlog.get_logger()
def test_library_add_alias(library: Library, generate_tag):
def test_library_add_alias(library: Library, generate_tag: Callable[..., Tag]):
tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
@@ -26,7 +38,7 @@ def test_library_add_alias(library: Library, generate_tag):
assert len(alias_ids) == 1
def test_library_get_alias(library: Library, generate_tag):
def test_library_get_alias(library: Library, generate_tag: Callable[..., Tag]):
tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
@@ -44,7 +56,7 @@ def test_library_get_alias(library: Library, generate_tag):
assert alias.name == "test_alias"
def test_library_update_alias(library: Library, generate_tag):
def test_library_update_alias(library: Library, generate_tag: Callable[..., Tag]):
tag: Tag | None = library.add_tag(generate_tag("xxx", id=123))
assert tag is not None
@@ -89,7 +101,7 @@ def test_library_add_file(library: Library):
assert library.has_path_entry(entry.path)
def test_create_tag(library: Library, generate_tag):
def test_create_tag(library: Library, generate_tag: Callable[..., Tag]):
# tag already exists
assert not library.add_tag(generate_tag("foo", id=1000))
@@ -103,7 +115,7 @@ def test_create_tag(library: Library, generate_tag):
assert tag_inc.id > 1000
def test_tag_self_parent(library: Library, generate_tag):
def test_tag_self_parent(library: Library, generate_tag: Callable[..., Tag]):
# tag already exists
assert not library.add_tag(generate_tag("foo", id=1000))
@@ -118,7 +130,7 @@ def test_tag_self_parent(library: Library, generate_tag):
assert len(tag.parent_ids) == 0
def test_library_search(library: Library, generate_tag, entry_full):
def test_library_search(library: Library, entry_full: Entry):
assert library.entries_count == 2
tag = list(entry_full.tags)[0]
@@ -140,7 +152,7 @@ def test_tag_search(library: Library):
assert library.search_tags(tag.name * 2) == [set(), set()]
def test_get_entry(library: Library, entry_min):
def test_get_entry(library: Library, entry_min: Entry):
assert entry_min.id
result = library.get_entry_full(entry_min.id)
assert result
@@ -159,12 +171,13 @@ def test_entries_count(library: Library):
assert len(results) == 5
def test_parents_add(library: Library, generate_tag):
def test_parents_add(library: Library, generate_tag: Callable[..., Tag]):
# Given
tag: Tag | None = library.tags[0]
assert tag.id is not None
parent_tag = generate_tag("parent_tag_01")
parent_tag: Tag | None = generate_tag("parent_tag_01")
assert parent_tag
parent_tag = library.add_tag(parent_tag)
assert parent_tag is not None
assert parent_tag.id is not None
@@ -179,7 +192,7 @@ def test_parents_add(library: Library, generate_tag):
assert tag.parent_ids
def test_remove_tag(library: Library, generate_tag):
def test_remove_tag(library: Library, generate_tag: Callable[..., Tag]):
tag = library.add_tag(generate_tag("food", id=123))
assert tag
@@ -237,7 +250,7 @@ def test_preferences(library: Library):
assert library.prefs(pref) == pref.default
def test_remove_entry_field(library: Library, entry_full):
def test_remove_entry_field(library: Library, entry_full: Entry):
title_field = entry_full.text_fields[0]
library.remove_entry_field(title_field, [entry_full.id])
@@ -246,7 +259,7 @@ def test_remove_entry_field(library: Library, entry_full):
assert not entry.text_fields
def test_remove_field_entry_with_multiple_field(library: Library, entry_full):
def test_remove_field_entry_with_multiple_field(library: Library, entry_full: Entry):
# Given
title_field = entry_full.text_fields[0]
@@ -262,7 +275,7 @@ def test_remove_field_entry_with_multiple_field(library: Library, entry_full):
assert len(entry.text_fields) == 1
def test_update_entry_field(library: Library, entry_full):
def test_update_entry_field(library: Library, entry_full: Entry):
title_field = entry_full.text_fields[0]
library.update_entry_field(
@@ -275,7 +288,7 @@ def test_update_entry_field(library: Library, entry_full):
assert entry.text_fields[0].value == "new value"
def test_update_entry_with_multiple_identical_fields(library: Library, entry_full):
def test_update_entry_with_multiple_identical_fields(library: Library, entry_full: Entry):
# Given
title_field = entry_full.text_fields[0]
@@ -296,7 +309,7 @@ def test_update_entry_with_multiple_identical_fields(library: Library, entry_ful
assert entry.text_fields[1].value == "new value"
def test_mirror_entry_fields(library: Library, entry_full):
def test_mirror_entry_fields(library: Library, entry_full: Entry):
# new entry
assert library.folder is not None
target_entry = Entry(
@@ -335,6 +348,14 @@ def test_mirror_entry_fields(library: Library, entry_full):
def test_merge_entries(library: Library):
assert library.folder is not None
tag_0: Tag | None = library.add_tag(Tag(id=1010, name="tag_0"))
assert tag_0 is not None
tag_1: Tag | None = library.add_tag(Tag(id=1011, name="tag_1"))
assert tag_1 is not None
tag_2: Tag | None = library.add_tag(Tag(id=1012, name="tag_2"))
assert tag_2 is not None
a = Entry(
folder=library.folder,
path=Path("a"),
@@ -348,32 +369,34 @@ def test_merge_entries(library: Library):
path=Path("b"),
fields=[TextField(type_key=_FieldID.NOTES.name, value="test note", position=1)],
)
try:
ids = library.add_entries([a, b])
entry_a = library.get_entry_full(ids[0])
assert entry_a is not None
entry_b = library.get_entry_full(ids[1])
assert entry_b is not None
tag_0 = library.add_tag(Tag(id=1000, name="tag_0"))
tag_1 = library.add_tag(Tag(id=1001, name="tag_1"))
assert tag_1 is not None
tag_2 = library.add_tag(Tag(id=1002, name="tag_2"))
assert tag_2 is not None
library.add_tags_to_entries(ids[0], [tag_0.id, tag_2.id])
library.add_tags_to_entries(ids[1], [tag_1.id])
library.merge_entries(entry_a, entry_b)
assert library.has_path_entry(Path("b"))
assert not library.has_path_entry(Path("a"))
fields = [field.value for field in entry_a.fields]
assert "Author McAuthorson" in fields
assert "test description" in fields
assert "test note" in fields
assert b.has_tag(tag_0) and b.has_tag(tag_1) and b.has_tag(tag_2)
except AttributeError:
AssertionError()
ids = library.add_entries([a, b])
library.add_tags_to_entries(ids[0], [tag_0.id, tag_2.id])
library.add_tags_to_entries(ids[1], [tag_1.id])
entry_a: Entry | None = library.get_entry_full(ids[0])
assert entry_a is not None
entry_b: Entry | None = library.get_entry_full(ids[1])
assert entry_b is not None
assert library.merge_entries(entry_a, entry_b)
assert not library.has_path_entry(Path("a"))
assert library.has_path_entry(Path("b"))
entry_b_merged = library.get_entry_full(ids[1])
assert entry_b_merged
fields = [field.value for field in entry_b_merged.fields]
assert "Author McAuthorson" in fields
assert "test description" in fields
assert "test note" in fields
b_tags = [t.id for t in entry_b_merged.tags]
assert tag_0.id in b_tags
assert tag_1.id in b_tags
assert tag_2.id in b_tags
def test_remove_tags_from_entries(library: Library, entry_full):
def test_remove_tags_from_entries(library: Library, entry_full: Entry):
removed_tag_id = -1
for tag in entry_full.tags:
removed_tag_id = tag.id
@@ -392,13 +415,13 @@ def test_remove_tags_from_entries(library: Library, entry_full):
(222, 0),
],
)
def test_search_entry_id(library: Library, query_name: int, has_result):
def test_search_entry_id(library: Library, query_name: int, has_result: bool):
result = library.get_entry(query_name)
assert (result is not None) == has_result
def test_update_field_order(library: Library, entry_full):
def test_update_field_order(library: Library, entry_full: Entry):
# Given
title_field = entry_full.text_fields[0]
@@ -526,13 +549,15 @@ def test_path_search_like_glob_equality(library: Library):
@pytest.mark.parametrize(["filetype", "num_of_filetype"], [("md", 1), ("txt", 1), ("png", 0)])
def test_filetype_search(library: Library, filetype, num_of_filetype):
def test_filetype_search(library: Library, filetype: str, num_of_filetype: int):
results = library.search_library(BrowsingState.from_filetype(filetype), page_size=500)
assert len(results.ids) == num_of_filetype
@pytest.mark.parametrize(["filetype", "num_of_filetype"], [("png", 2), ("apng", 1), ("ng", 0)])
def test_filetype_return_one_filetype(file_mediatypes_library: Library, filetype, num_of_filetype):
def test_filetype_return_one_filetype(
file_mediatypes_library: Library, filetype: str, num_of_filetype: int
):
results = file_mediatypes_library.search_library(
BrowsingState.from_filetype(filetype), page_size=500
)
@@ -540,6 +565,6 @@ def test_filetype_return_one_filetype(file_mediatypes_library: Library, filetype
@pytest.mark.parametrize(["mediatype", "num_of_mediatype"], [("plaintext", 2), ("image", 0)])
def test_mediatype_search(library: Library, mediatype, num_of_mediatype):
def test_mediatype_search(library: Library, mediatype: str, num_of_mediatype: int):
results = library.search_library(BrowsingState.from_mediatype(mediatype), page_size=500)
assert len(results.ids) == num_of_mediatype

View File

@@ -1,3 +1,8 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import pytest
import structlog
@@ -139,5 +144,5 @@ def test_parent_tags(search_library: Library, query: str, count: int):
"invalid_query", ["asd AND", "asd AND AND", "tag:(", "(asd", "asd[]", "asd]", ":", "tag: :"]
)
def test_syntax(search_library: Library, invalid_query: str):
with pytest.raises(ParsingError) as e_info: # noqa: F841
with pytest.raises(ParsingError) as e_info: # noqa: F841 # pyright: ignore[reportUnusedVariable]
search_library.search_library(BrowsingState.from_search_query(invalid_query), page_size=500)

View File

@@ -1,3 +1,8 @@
# Copyright (C) 2025
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import string
from pathlib import Path