refactor!: change layout; renaming, import and build change incoming

This commit is contained in:
Xarvex
2025-03-06 18:35:46 -06:00
parent 981cc60135
commit 226d18e743
250 changed files with 0 additions and 0 deletions

166
tests/conftest.py Normal file
View File

@@ -0,0 +1,166 @@
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import Mock, patch
import pytest
CWD = Path(__file__).parent
# this needs to be above `src` imports
sys.path.insert(0, str(CWD.parent))
from src.core.library import Entry, Library, Tag
from src.core.library import alchemy as backend
from src.qt.ts_qt import QtDriver
@pytest.fixture
def cwd():
return CWD
@pytest.fixture
def file_mediatypes_library():
lib = Library()
status = lib.open_library(Path(""), ":memory:")
assert status.success
entry1 = Entry(
folder=lib.folder,
path=Path("foo.png"),
fields=lib.default_fields,
)
entry2 = Entry(
folder=lib.folder,
path=Path("bar.png"),
fields=lib.default_fields,
)
entry3 = Entry(
folder=lib.folder,
path=Path("baz.apng"),
fields=lib.default_fields,
)
assert lib.add_entries([entry1, entry2, entry3])
assert len(lib.tags) == 3
return lib
@pytest.fixture
def library(request):
# when no param is passed, use the default
library_path = "/dev/null/"
if hasattr(request, "param"):
if isinstance(request.param, TemporaryDirectory):
library_path = request.param.name
else:
library_path = request.param
lib = Library()
status = lib.open_library(Path(library_path), ":memory:")
assert status.success
tag = Tag(
name="foo",
color_namespace="tagstudio-standard",
color_slug="red",
)
assert lib.add_tag(tag)
parent_tag = Tag(
id=1500,
name="subbar",
color_namespace="tagstudio-standard",
color_slug="yellow",
)
assert lib.add_tag(parent_tag)
tag2 = Tag(
id=2000,
name="bar",
color_namespace="tagstudio-standard",
color_slug="blue",
parent_tags={parent_tag},
)
assert lib.add_tag(tag2)
# default item with deterministic name
entry = Entry(
id=1,
folder=lib.folder,
path=Path("foo.txt"),
fields=lib.default_fields,
)
assert lib.add_tags_to_entries(entry.id, tag.id)
entry2 = Entry(
id=2,
folder=lib.folder,
path=Path("one/two/bar.md"),
fields=lib.default_fields,
)
assert lib.add_tags_to_entries(entry2.id, tag2.id)
assert lib.add_entries([entry, entry2])
assert len(lib.tags) == 6
yield lib
@pytest.fixture
def search_library() -> Library:
lib = Library()
lib.open_library(Path(CWD / "fixtures" / "search_library"))
return lib
@pytest.fixture
def entry_min(library):
yield next(library.get_entries())
@pytest.fixture
def entry_full(library: Library):
yield next(library.get_entries(with_joins=True))
@pytest.fixture
def qt_driver(qtbot, library):
with TemporaryDirectory() as tmp_dir:
class Args:
config_file = Path(tmp_dir) / "tagstudio.ini"
open = Path(tmp_dir)
ci = True
with patch("src.qt.ts_qt.Consumer"), patch("src.qt.ts_qt.CustomRunnable"):
driver = QtDriver(backend, Args())
driver.main_window = Mock()
driver.preview_panel = Mock()
driver.flow_container = Mock()
driver.item_thumbs = []
driver.autofill_action = Mock()
driver.copy_buffer = {"fields": [], "tags": []}
driver.copy_fields_action = Mock()
driver.paste_fields_action = Mock()
driver.lib = library
# TODO - downsize this method and use it
# driver.start()
driver.frame_content = list(library.get_entries())
yield driver
@pytest.fixture
def generate_tag():
def inner(name, **kwargs):
params = dict(name=name, color_namespace="tagstudio-standard", color_slug="red") | kwargs
return Tag(**params)
yield inner

File diff suppressed because one or more lines are too long

10
tests/fixtures/result.dupeguru vendored Normal file
View File

@@ -0,0 +1,10 @@
<results>
<group>
<file path="/tmp/bar/foo.txt" words="" is_ref="n" marked="n"/>
<file path="/tmp/foo.txt" words="" is_ref="n" marked="n"/>
<file path="/tmp/foo/foo.txt" words="" is_ref="n" marked="n"/>
<match first="1" second="0" percentage="100"/>
<match first="0" second="2" percentage="100"/>
<match first="1" second="2" percentage="100"/>
</group>
</results>

BIN
tests/fixtures/sample.epub vendored Normal file

Binary file not shown.

BIN
tests/fixtures/sample.ods vendored Normal file

Binary file not shown.

BIN
tests/fixtures/sample.odt vendored Normal file

Binary file not shown.

BIN
tests/fixtures/sample.pdf vendored Normal file

Binary file not shown.

8
tests/fixtures/sample.svg vendored Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 739 739" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M392.721,715.585C422.869,745.733 471.822,745.733 501.97,715.585C521.252,696.304 698.205,519.351 715.585,501.97C745.733,471.822 745.733,422.869 715.585,392.721C702.356,379.491 380.066,58.201 366.836,44.972C353.638,31.774 336.836,24.354 319.586,22.707C305.002,21.315 93.263,0.703 81.113,0.097C60.067,-0.954 38.675,6.558 22.616,22.616C7.307,37.926 -0.233,58.083 0.005,78.166C0.158,90.957 21.834,301.853 22.429,315.577C23.238,334.217 30.753,352.617 44.972,366.836C64.714,386.578 372.979,695.844 392.721,715.585ZM221.206,462.624C199.538,440.957 109.62,350.947 109.62,350.947C95.375,336.702 95.375,315.292 109.62,301.047L301.047,109.62C315.292,95.375 336.702,95.375 350.947,109.62C350.947,109.62 557.592,316.079 616.139,374.813C679.714,438.589 568.691,438.77 585.773,495.797C588.986,506.52 600.846,531.699 594.432,548.233C579.351,587.118 529.349,575.43 521.799,548.233C515.39,525.148 528.92,514.925 524.759,499.813C517.88,474.829 491.657,481.134 483.903,499.813C458.989,559.836 503.029,591.37 493.423,628.195C479.788,680.463 410.137,673.342 400.675,628.195C392.608,589.708 425.265,574.286 421.93,520.583C419.956,488.804 394.552,464.882 377.567,488.023C360.792,510.879 402.256,542.428 373.64,575.065C341.566,611.644 283.344,579.898 301.047,528.499C311.597,497.869 326.263,466.649 311.901,462.624C282.374,454.351 267.942,509.361 221.206,462.624ZM180.995,84.554C205.968,109.528 205.968,150.079 180.995,175.052C156.022,200.026 115.471,200.026 90.497,175.052C65.524,150.079 65.524,109.528 90.497,84.554C115.471,59.581 156.022,59.581 180.995,84.554Z"/>
<g transform="matrix(0.986683,0,0,0.986683,-136.081,-136.081)">
<path d="M733.962,560.164C740.987,553.139 740.987,541.733 733.962,534.708L482.173,282.92C475.148,275.895 463.742,275.895 456.717,282.92L431.261,308.376C424.237,315.4 424.237,326.807 431.261,333.831L683.05,585.62C690.075,592.645 701.481,592.645 708.506,585.62L733.962,560.164ZM439.639,559.207C446.664,552.182 446.664,540.776 439.639,533.751L335.491,429.602C328.466,422.578 317.059,422.578 310.035,429.602L284.579,455.058C277.554,462.083 277.554,473.489 284.579,480.514L388.728,584.663C395.752,591.688 407.159,591.688 414.184,584.663L439.639,559.207ZM584.306,556.624C591.331,549.599 591.331,538.192 584.306,531.168L409.115,355.977C402.091,348.953 390.684,348.953 383.66,355.977L358.204,381.433C351.179,388.458 351.179,399.864 358.204,406.889L533.394,582.079C540.419,589.104 551.825,589.104 558.85,582.079L584.306,556.624ZM298.425,246.543C311.081,259.198 311.081,279.747 298.425,292.402C285.77,305.058 265.221,305.058 252.566,292.402C239.911,279.747 239.911,259.198 252.566,246.543C265.221,233.888 285.77,233.888 298.425,246.543Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,431 @@
{
"ts-version": "9.4.2",
"ext_list": [".json", ".xmp", ".aae", ".txt"],
"is_exclude_list": true,
"tags": [
{ "id": 0, "name": "Archived", "aliases": ["Archive"], "color": "Red" },
{
"id": 1,
"name": "Favorite",
"aliases": ["Favorited", "Favorites"],
"color": "Yellow"
},
{ "id": 1000, "name": "Parent", "aliases": [""], "subtag_ids": [1000] },
{ "id": 1001, "name": "Default", "aliases": [""] },
{
"id": 1002,
"name": "Black",
"aliases": [""],
"subtag_ids": [1040],
"color": "black"
},
{
"id": 1003,
"name": "Dark Gray",
"aliases": ["Dark Grey"],
"subtag_ids": [1040, 1002, 1004],
"color": "dark gray"
},
{
"id": 1004,
"name": "Gray",
"aliases": ["Grey"],
"subtag_ids": [1040, 1002, 1006],
"color": "gray"
},
{
"id": 1005,
"name": "Light Gray",
"aliases": ["Light Grey"],
"subtag_ids": [1040, 1006, 1004],
"color": "light gray"
},
{
"id": 1006,
"name": "White",
"aliases": [""],
"subtag_ids": [1040],
"color": "white"
},
{
"id": 1007,
"name": "Light Pink",
"aliases": [""],
"subtag_ids": [1040, 1009, 1006],
"color": "light pink"
},
{
"id": 1008,
"name": "Pink",
"aliases": [""],
"subtag_ids": [1040, 1006, 1009],
"color": "pink"
},
{
"id": 1009,
"name": "Red",
"aliases": [""],
"subtag_ids": [1040],
"color": "red"
},
{
"id": 1010,
"name": "Red Orange",
"aliases": [""],
"subtag_ids": [1040, 1009, 1011],
"color": "red orange"
},
{
"id": 1011,
"name": "Orange",
"aliases": [""],
"subtag_ids": [1040, 1009, 1013],
"color": "orange"
},
{
"id": 1012,
"name": "Yellow Orange",
"aliases": [""],
"subtag_ids": [1040, 1011],
"color": "yellow orange"
},
{
"id": 1013,
"name": "Yellow",
"aliases": [""],
"subtag_ids": [1040],
"color": "yellow"
},
{
"id": 1014,
"name": "Lime",
"aliases": [""],
"subtag_ids": [1040, 1017, 1006],
"color": "lime"
},
{
"id": 1015,
"name": "Light Green",
"aliases": [""],
"color": "light green"
},
{
"id": 1016,
"name": "Mint",
"aliases": [""],
"subtag_ids": [1040, 1017, 1019],
"color": "mint"
},
{
"id": 1017,
"name": "Green",
"aliases": [""],
"subtag_ids": [1040, 1021, 1013],
"color": "green"
},
{
"id": 1018,
"name": "Teal",
"aliases": [""],
"subtag_ids": [1040, 1017, 1021],
"color": "teal"
},
{
"id": 1019,
"name": "Cyan",
"aliases": [""],
"subtag_ids": [1040, 1017, 1021],
"color": "cyan"
},
{
"id": 1020,
"name": "Light Blue",
"aliases": [""],
"subtag_ids": [1040, 1021, 1006],
"color": "light blue"
},
{
"id": 1021,
"name": "Blue",
"aliases": [""],
"subtag_ids": [1040],
"color": "blue"
},
{
"id": 1022,
"name": "Blue Violet",
"aliases": [""],
"subtag_ids": [1040, 1021, 1023],
"color": "blue violet"
},
{
"id": 1023,
"name": "Violet",
"aliases": [""],
"subtag_ids": [1040, 1009, 1021],
"color": "violet"
},
{
"id": 1024,
"name": "Purple",
"aliases": [""],
"subtag_ids": [1040, 1009, 1021],
"color": "purple"
},
{
"id": 1025,
"name": "Lavender",
"aliases": [""],
"subtag_ids": [1040, 1024, 1006],
"color": "lavender"
},
{ "id": 1026, "name": "Berry", "aliases": [""], "color": "berry" },
{ "id": 1027, "name": "Magenta", "aliases": [""], "color": "magenta" },
{ "id": 1028, "name": "Salmon", "aliases": [""], "color": "salmon" },
{ "id": 1029, "name": "Auburn", "aliases": [""], "color": "auburn" },
{
"id": 1030,
"name": "Dark Brown",
"aliases": [""],
"color": "dark brown"
},
{ "id": 1031, "name": "Brown", "aliases": [""], "color": "brown" },
{
"id": 1032,
"name": "Light Brown",
"aliases": [""],
"color": "light brown"
},
{ "id": 1033, "name": "Blonde", "aliases": [""], "color": "blonde" },
{ "id": 1034, "name": "Peach", "aliases": [""], "color": "peach" },
{
"id": 1035,
"name": "Warm Gray",
"aliases": ["Warm Grey"],
"subtag_ids": [1040, 1004, 1011],
"color": "warm gray"
},
{
"id": 1036,
"name": "Cool Gray",
"aliases": ["Cool Grey"],
"subtag_ids": [1040, 1004, 1021],
"color": "cool gray"
},
{
"id": 1037,
"name": "Olive",
"aliases": [""],
"subtag_ids": [1040, 1017, 1004],
"color": "olive"
},
{ "id": 1038, "name": "Square", "aliases": [""], "subtag_ids": [1039] },
{ "id": 1039, "name": "Shape", "aliases": [""] },
{ "id": 1040, "name": "Color", "aliases": [""] },
{
"id": 1041,
"name": "Circle",
"aliases": [""],
"subtag_ids": [1039, 1042]
},
{
"id": 1042,
"name": "Ellipse",
"aliases": [""],
"subtag_ids": [1039, 1043]
},
{ "id": 1043, "name": "Round", "aliases": [""] }
],
"collations": [],
"fields": [],
"macros": [],
"entries": [
{
"id": 0,
"filename": "red.jpg",
"path": "inherit colors shapes",
"fields": [{ "6": [1009] }]
},
{
"id": 1,
"filename": "red_square.jpg",
"path": "inherit colors shapes",
"fields": [{ "6": [1009, 1038] }]
},
{
"id": 2,
"filename": "red_circle.jpg",
"path": "inherit colors shapes",
"fields": [{ "6": [1041, 1009] }]
},
{
"id": 3,
"filename": "blue_circle.jpg",
"path": "inherit colors shapes",
"fields": [{ "6": [1021, 1041] }]
},
{
"id": 4,
"filename": "blue_square.jpg",
"path": "inherit colors shapes",
"fields": [{ "6": [1021, 1038] }]
},
{
"id": 5,
"filename": "blue.jpg",
"path": "inherit colors shapes",
"fields": [{ "6": [1021] }]
},
{
"id": 10,
"filename": "green_circle.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1041, 1017] }]
},
{
"id": 11,
"filename": "green.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1017] }]
},
{
"id": 12,
"filename": "green_square.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1017, 1038] }]
},
{
"id": 13,
"filename": "yellow_circle.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1041, 1013] }]
},
{
"id": 14,
"filename": "yellow_square.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1038, 1013] }]
},
{
"id": 15,
"filename": "yellow.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1013] }]
},
{
"id": 16,
"filename": "square.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1038] }]
},
{
"id": 17,
"filename": "circle.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1041] }]
},
{
"id": 18,
"filename": "shape.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1039] }]
},
{
"id": 19,
"filename": "orange_circle.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1041, 1011] }]
},
{
"id": 20,
"filename": "orange_square.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1011, 1038] }]
},
{
"id": 21,
"filename": "orange.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1011] }]
},
{
"id": 22,
"filename": "yellow_ellipse.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1042, 1013] }]
},
{
"id": 23,
"filename": "ellipse.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1042] }]
},
{
"id": 24,
"filename": "red_ellipse.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1042, 1009] }]
},
{
"id": 25,
"filename": "blue_ellipse.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1021, 1042] }]
},
{
"id": 26,
"filename": "green_ellipse.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1042, 1017] }]
},
{
"id": 27,
"filename": "orange_ellipse.png",
"path": "inherit colors shapes",
"fields": [{ "6": [1042, 1011] }]
},
{
"id": 30,
"filename": "r_circle_b_square.png",
"path": "comp colors shapes",
"fields": [{ "6": [1021, 1041, 1009, 1038] }]
},
{
"id": 31,
"filename": "r_circle_g_square.png",
"path": "comp colors shapes",
"fields": [{ "6": [1041, 1017, 1009, 1038] }]
},
{
"id": 32,
"filename": "r_circle_y_square.png",
"path": "comp colors shapes",
"fields": [{ "6": [1041, 1009, 1038, 1013] }]
},
{
"id": 33,
"filename": "r_circle_o_square.png",
"path": "comp colors shapes",
"fields": [{ "6": [1041, 1011, 1009, 1038] }]
},
{
"id": 34,
"filename": "r_circle_r_square.png",
"path": "comp colors shapes",
"fields": [{ "6": [1041, 1009, 1038] }]
},
{
"id": 35,
"filename": "untagged.txt",
"path": ".",
"fields": [{ "0": "" }]
},
{
"id": 36,
"filename": "untagged.png",
"path": ".",
"fields": [{ "0": "I have fields, but no tags. I am not empty." }]
},
{ "id": 37, "filename": "empty.png", "path": "." }
]
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

10
tests/fixtures/sidecar_newgrounds.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"tags": [
"ng_tag",
"ng_tag2"
],
"date": "2024-01-02",
"description": "NG description",
"user": "NG artist",
"post_url": "https://ng.com"
}

View File

@@ -0,0 +1,36 @@
from pathlib import Path
from src.core.library import Entry
from src.core.utils.dupe_files import DupeRegistry
CWD = Path(__file__).parent
def test_refresh_dupe_files(library):
library.library_dir = "/tmp/"
entry = Entry(
folder=library.folder,
path=Path("bar/foo.txt"),
fields=library.default_fields,
)
entry2 = Entry(
folder=library.folder,
path=Path("foo/foo.txt"),
fields=library.default_fields,
)
library.add_entries([entry, entry2])
registry = DupeRegistry(library=library)
dupe_file_path = CWD.parent / "fixtures" / "result.dupeguru"
registry.refresh_dupe_files(dupe_file_path)
assert len(registry.groups) == 1
paths = [entry.path for entry in registry.groups[0]]
assert paths == [
Path("bar/foo.txt"),
Path("foo.txt"),
Path("foo/foo.txt"),
]

View File

@@ -0,0 +1,7 @@
from src.qt.modals.folders_to_tags import folders_to_tags
def test_folders_to_tags(library):
folders_to_tags(library)
entry = [x for x in library.get_entries(with_joins=True) if "bar.md" in str(x.path)][0]
assert {x.name for x in entry.tags} == {"two", "bar"}

View File

@@ -0,0 +1,31 @@
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from src.core.library import Library
from src.core.library.alchemy.enums import FilterState
from src.core.utils.missing_files import MissingRegistry
CWD = Path(__file__).parent
# NOTE: Does this test actually work?
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
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
(library.library_dir / "bar.md").touch()
# no files actually exist, so it should return all entries
assert list(registry.refresh_missing_files()) == [0, 1]
# neither of the library entries exist
assert len(registry.missing_file_entries) == 2
# iterate through two files
assert list(registry.fix_unlinked_entries()) == [0, 1]
# `bar.md` should be relinked to new correct path
results = library.search_library(FilterState.from_path("bar.md"))
assert results[0].path == Path("bar.md")

View File

@@ -0,0 +1,25 @@
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from src.core.enums import LibraryPrefs
from src.core.utils.refresh_dir import RefreshDirTracker
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):
# Given
library.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, exclude_mode)
library.set_prefs(LibraryPrefs.EXTENSION_LIST, [".md"])
registry = RefreshDirTracker(library=library)
library.included_files.clear()
(library.library_dir / "FOO.MD").touch()
# When
assert len(list(registry.refresh_dir(library.library_dir))) == 1
# Then
assert registry.files_not_in_library == [Path("FOO.MD")]

View File

@@ -0,0 +1,37 @@
# import shutil
# from pathlib import Path
# from tempfile import TemporaryDirectory
# import pytest
# from src.core.enums import MacroID
# from src.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

@@ -0,0 +1,4 @@
# serializer version: 1
# name: test_generate_preview_data
BranchData(dirs={'two': BranchData(dirs={}, files=['bar.md'], tag=<Tag ID: None Name: two>)}, files=[], tag=None)
# ---

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,159 @@
from src.core.library.alchemy.models import Tag
from src.qt.modals.build_tag import BuildTagPanel
from src.qt.translations import Translations
def test_build_tag_panel_add_sub_tag_callback(library, generate_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)
panel.add_parent_tag_callback(parent.id)
assert len(panel.parent_ids) == 1
def test_build_tag_panel_remove_subtag_callback(library, generate_tag):
parent = library.add_tag(generate_tag("xxx", id=123))
child = library.add_tag(generate_tag("xx", id=124))
assert child
assert parent
library.update_tag(child, {parent.id}, [], [])
child = library.get_tag(child.id)
assert child
panel: BuildTagPanel = BuildTagPanel(library, child)
panel.remove_parent_tag_callback(parent.id)
assert len(panel.parent_ids) == 0
import os
os.environ["QT_QPA_PLATFORM"] = "offscreen"
def test_build_tag_panel_add_alias_callback(library, generate_tag):
tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
panel: BuildTagPanel = BuildTagPanel(library, tag)
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))
assert tag
library.update_tag(tag, [], {"alias", "alias_2"}, {123, 124})
tag = library.get_tag(tag.id)
assert "alias" in tag.alias_strings
assert "alias_2" in tag.alias_strings
panel: BuildTagPanel = BuildTagPanel(library, tag)
alias = library.get_alias(tag.id, tag.alias_ids[0])
panel.remove_alias_callback(alias.name, alias.id)
assert len(panel.alias_ids) == 1
assert len(panel.alias_names) == 1
assert alias.name not in panel.alias_names
def test_build_tag_panel_set_parent_tags(library, generate_tag):
parent = library.add_tag(generate_tag("parent", id=123))
child = library.add_tag(generate_tag("child", id=124))
assert parent
assert child
library.add_parent_tag(child.id, parent.id)
child = library.get_tag(child.id)
panel: BuildTagPanel = BuildTagPanel(library, child)
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))
assert tag
library.update_tag(tag, [], {"alias", "alias_2"}, {123, 124})
tag = library.get_tag(tag.id)
assert "alias" in tag.alias_strings
assert "alias_2" in tag.alias_strings
panel: BuildTagPanel = BuildTagPanel(library, tag)
widget = panel.aliases_table.cellWidget(0, 1)
alias_names: set[str] = set()
alias_names.add(widget.text())
widget = panel.aliases_table.cellWidget(1, 1)
alias_names.add(widget.text())
assert "alias" in alias_names
assert "alias_2" in alias_names
old_text = widget.text()
widget.setText("alias_update")
panel.add_aliases()
assert old_text not in panel.alias_names
assert "alias_update" in panel.alias_names
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))
assert tag
library.update_tag(tag, [], {"alias"}, {123})
tag = library.get_tag(tag.id)
assert len(tag.alias_ids) == 1
panel: BuildTagPanel = BuildTagPanel(library, tag)
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):
tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
panel: BuildTagPanel = BuildTagPanel(library, tag)
assert panel.tag
assert panel.tag.name == "xxx"
def test_build_tag_panel_build_tag(library):
panel: BuildTagPanel = BuildTagPanel(library)
tag: Tag = panel.build_tag()
assert tag
assert tag.name == Translations["tag.new"]

View File

@@ -0,0 +1,172 @@
from src.qt.widgets.preview_panel import PreviewPanel
def test_update_selection_empty(qt_driver, library):
panel = PreviewPanel(library, qt_driver)
# Clear the library selection (selecting 1 then unselecting 1)
qt_driver.toggle_item_selection(1, append=False, bridge=False)
qt_driver.toggle_item_selection(1, append=True, bridge=False)
panel.update_widgets()
# FieldContainer should hide all containers
for container in panel.fields.containers:
assert container.isHidden()
def test_update_selection_single(qt_driver, library, entry_full):
panel = PreviewPanel(library, qt_driver)
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
# FieldContainer should show all applicable tags and field containers
for container in panel.fields.containers:
assert not container.isHidden()
def test_update_selection_multiple(qt_driver, library):
# TODO: Implement mixed field editing. Currently these containers will be hidden,
# same as the empty selection behavior.
panel = PreviewPanel(library, qt_driver)
# Select the multiple entries
qt_driver.toggle_item_selection(1, append=False, bridge=False)
qt_driver.toggle_item_selection(2, append=True, bridge=False)
panel.update_widgets()
# FieldContainer should show mixed field editing
for container in panel.fields.containers:
assert container.isHidden()
def test_add_tag_to_selection_single(qt_driver, library, entry_full):
panel = PreviewPanel(library, qt_driver)
assert {t.id for t in entry_full.tags} == {1000}
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
# Add new tag
panel.fields.add_tags_to_selected(2000)
# Then reload entry
refreshed_entry = next(library.get_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):
panel = PreviewPanel(library, qt_driver)
assert {t.id for t in entry_full.tags} == {1000}
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
# Add an existing tag
panel.fields.add_tags_to_selected(1000)
# Then reload entry
refreshed_entry = next(library.get_entries(with_joins=True))
assert {t.id for t in refreshed_entry.tags} == {1000}
def test_add_tag_to_selection_multiple(qt_driver, library):
panel = PreviewPanel(library, qt_driver)
all_entries = library.get_entries(with_joins=True)
# We want to verify that tag 1000 is on some, but not all entries already.
tag_present_on_some: bool = False
tag_absent_on_some: bool = False
for e in all_entries:
if 1000 in [t.id for t in e.tags]:
tag_present_on_some = True
else:
tag_absent_on_some = True
assert tag_present_on_some
assert tag_absent_on_some
# Select the multiple entries
for i, e in enumerate(library.get_entries(with_joins=True), start=0):
qt_driver.toggle_item_selection(e.id, append=(True if i == 0 else False), bridge=False) # noqa: SIM210
panel.update_widgets()
# Add new tag
panel.fields.add_tags_to_selected(1000)
# Then reload all entries and recheck the presence of tag 1000
refreshed_entries = library.get_entries(with_joins=True)
tag_present_on_some: bool = False
tag_absent_on_some: bool = False
for e in refreshed_entries:
if 1000 in [t.id for t in e.tags]:
tag_present_on_some = True
else:
tag_absent_on_some = True
assert tag_present_on_some
assert not tag_absent_on_some
def test_meta_tag_category(qt_driver, library, entry_full):
panel = PreviewPanel(library, qt_driver)
# Ensure the Favorite tag is on entry_full
library.add_tags_to_entries(1, entry_full.id)
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
# FieldContainer should hide all containers
assert len(panel.fields.containers) == 3
for i, container in enumerate(panel.fields.containers):
match i:
case 0:
# Check if the container is the Meta Tags category
assert container.title == f"<h4>{library.get_tag(2).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>"
def test_custom_tag_category(qt_driver, library, entry_full):
panel = PreviewPanel(library, qt_driver)
# Set tag 1000 (foo) as a category
tag = library.get_tag(1000)
tag.is_category = True
library.update_tag(
tag,
)
# Ensure the Favorite tag is on entry_full
library.add_tags_to_entries(1, entry_full.id)
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
# FieldContainer should hide all containers
assert len(panel.fields.containers) == 3
for i, container in enumerate(panel.fields.containers):
match i:
case 0:
# Check if the container is the Meta Tags category
assert container.title == f"<h4>{library.get_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>"

View File

@@ -0,0 +1,17 @@
from PySide6.QtCore import QRect
from PySide6.QtWidgets import QPushButton, QWidget
from src.qt.flowlayout import FlowLayout
def test_flow_layout_happy_path(qtbot):
class Window(QWidget):
def __init__(self):
super().__init__()
self.flow_layout = FlowLayout(self)
self.flow_layout.enable_grid_optimizations(value=True)
self.flow_layout.addWidget(QPushButton("Short"))
window = Window()
assert window.flow_layout.count()
assert window.flow_layout._do_layout(QRect(0, 0, 0, 0), test_only=False)

View File

@@ -0,0 +1,7 @@
from src.qt.modals.folders_to_tags import generate_preview_data
def test_generate_preview_data(library, snapshot):
preview = generate_preview_data(library)
assert preview == snapshot

View File

@@ -0,0 +1,18 @@
import pytest
from src.core.library import ItemType
from src.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)
qt_driver.frame_content = [entry_min]
qt_driver.selected = [0]
qt_driver.item_thumbs = [thumb]
thumb.badges[BadgeType.FAVORITE].setChecked(new_value)
assert thumb.badges[BadgeType.FAVORITE].isChecked() == new_value
# TODO
# assert thumb.favorite_badge.isHidden() == initial_state
assert thumb.is_favorite == new_value

View File

@@ -0,0 +1,39 @@
from src.qt.widgets.preview_panel import PreviewPanel
def test_update_selection_empty(qt_driver, library):
panel = PreviewPanel(library, qt_driver)
# Clear the library selection (selecting 1 then unselecting 1)
qt_driver.toggle_item_selection(1, append=False, bridge=False)
qt_driver.toggle_item_selection(1, append=True, bridge=False)
panel.update_widgets()
# Panel should disable UI that allows for entry modification
assert not panel.add_tag_button.isEnabled()
assert not panel.add_field_button.isEnabled()
def test_update_selection_single(qt_driver, library, entry_full):
panel = PreviewPanel(library, qt_driver)
# Select the single entry
qt_driver.toggle_item_selection(entry_full.id, append=False, bridge=False)
panel.update_widgets()
# Panel should enable UI that allows for entry modification
assert panel.add_tag_button.isEnabled()
assert panel.add_field_button.isEnabled()
def test_update_selection_multiple(qt_driver, library):
panel = PreviewPanel(library, qt_driver)
# Select the multiple entries
qt_driver.toggle_item_selection(1, append=False, bridge=False)
qt_driver.toggle_item_selection(2, append=True, bridge=False)
panel.update_widgets()
# Panel should enable UI that allows for entry modification
assert panel.add_tag_button.isEnabled()
assert panel.add_field_button.isEnabled()

110
tests/qt/test_qt_driver.py Normal file
View File

@@ -0,0 +1,110 @@
from src.core.library.alchemy.enums import FilterState
from src.core.library.json.library import ItemType
from src.qt.widgets.item_thumb import ItemThumb
# 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_library_state_update(qt_driver):
# Given
for entry in qt_driver.lib.get_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)
# no filter, both items are returned
qt_driver.filter_items()
assert len(qt_driver.frame_content) == 2
# filter by tag
state = FilterState.from_tag_name("foo").with_page_size(10)
qt_driver.filter_items(state)
assert qt_driver.filter.page_size == 10
assert len(qt_driver.frame_content) == 1
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
assert list(entry.tags)[0].name == "foo"
# When state is not changed, previous one is still applied
qt_driver.filter_items()
assert qt_driver.filter.page_size == 10
assert len(qt_driver.frame_content) == 1
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
assert list(entry.tags)[0].name == "foo"
# When state property is changed, previous one is overwritten
state = FilterState.from_path("*bar.md")
qt_driver.filter_items(state)
assert len(qt_driver.frame_content) == 1
entry = qt_driver.lib.get_entry_full(qt_driver.frame_content[0])
assert list(entry.tags)[0].name == "bar"
def test_close_library(qt_driver):
# Given
qt_driver.close_library()
# Then
assert qt_driver.lib.library_dir is None
assert not qt_driver.frame_content
assert not qt_driver.selected
assert not any(x.mode for x in qt_driver.item_thumbs)
# close library again to see there's no error
qt_driver.close_library()
qt_driver.close_library(is_shutdown=True)

View File

@@ -0,0 +1,24 @@
from src.core.library import Tag
from src.qt.modals.build_tag import BuildTagPanel
def test_tag_panel(qtbot, library):
panel = BuildTagPanel(library)
qtbot.addWidget(panel)
def test_add_tag_callback(qt_driver):
# Given
assert len(qt_driver.lib.tags) == 6
qt_driver.add_tag_action_callback()
# When
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
assert len(tags) == 7
assert "xxx" in {tag.name for tag in tags}

View File

@@ -0,0 +1,11 @@
from src.qt.modals.tag_search import TagSearchPanel
def test_update_tags(qtbot, library):
# Given
panel = TagSearchPanel(library)
qtbot.addWidget(panel)
# When
panel.update_tags()

View File

@@ -0,0 +1,48 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import io
from functools import partial
from pathlib import Path
import pytest
from PIL import Image
from src.qt.widgets.thumb_renderer import ThumbRenderer
from syrupy.extensions.image import PNGImageSnapshotExtension
@pytest.mark.parametrize(
["fixture_file", "thumbnailer"],
[
(
"sample.odt",
ThumbRenderer._open_doc_thumb,
),
(
"sample.ods",
ThumbRenderer._open_doc_thumb,
),
(
"sample.epub",
ThumbRenderer._epub_cover,
),
(
"sample.pdf",
partial(ThumbRenderer._pdf_thumb, size=200),
),
(
"sample.svg",
partial(ThumbRenderer._image_vector_thumb, size=200),
),
],
)
def test_preview_render(cwd, fixture_file, thumbnailer, snapshot):
file_path: Path = cwd / "fixtures" / fixture_file
img: Image.Image = thumbnailer(file_path)
img_bytes = io.BytesIO()
img.save(img_bytes, format="PNG")
img_bytes.seek(0)
assert img_bytes.read() == snapshot(extension_class=PNGImageSnapshotExtension)

View File

@@ -0,0 +1,47 @@
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import shutil
from pathlib import Path
import pytest
from src.core.constants import TS_FOLDER_NAME
from src.core.library.alchemy.library import Library
CWD = Path(__file__)
FIXTURES = "fixtures"
EMPTY_LIBRARIES = "empty_libraries"
@pytest.mark.parametrize(
"path",
[
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_6")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_7")),
str(Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_8")),
],
)
def test_library_migrations(path: str):
library = Library()
# Copy libraries to temp dir so modifications don't show up in version control
original_path = Path(path)
temp_path = Path(CWD.parent / FIXTURES / EMPTY_LIBRARIES / "DB_VERSION_TEMP")
temp_path.mkdir(exist_ok=True)
temp_path_ts = temp_path / TS_FOLDER_NAME
temp_path_ts.mkdir(exist_ok=True)
shutil.copy(
original_path / TS_FOLDER_NAME / Library.SQL_FILENAME,
temp_path / TS_FOLDER_NAME / Library.SQL_FILENAME,
)
try:
status = library.open_library(library_dir=temp_path)
library.close()
shutil.rmtree(temp_path)
assert status.success
except Exception as e:
library.close()
shutil.rmtree(temp_path)
raise (e)

69
tests/test_driver.py Normal file
View File

@@ -0,0 +1,69 @@
from os import makedirs
from pathlib import Path
from tempfile import TemporaryDirectory
from PySide6.QtCore import QSettings
from src.core.constants import TS_FOLDER_NAME
from src.core.driver import DriverMixin
from src.core.enums import SettingItems
from src.core.library.alchemy.library import LibraryStatus
class TestDriver(DriverMixin):
def __init__(self, settings):
self.settings = settings
def test_evaluate_path_empty():
# Given
settings = QSettings()
driver = TestDriver(settings)
# When
result = driver.evaluate_path(None)
# Then
assert result == LibraryStatus(success=True)
def test_evaluate_path_missing():
# Given
settings = QSettings()
driver = TestDriver(settings)
# When
result = driver.evaluate_path("/0/4/5/1/")
# Then
assert result == LibraryStatus(success=False, message="Path does not exist.")
def test_evaluate_path_last_lib_not_exists():
# Given
settings = QSettings()
settings.setValue(SettingItems.LAST_LIBRARY, "/0/4/5/1/")
driver = TestDriver(settings)
# When
result = driver.evaluate_path(None)
# Then
assert result == LibraryStatus(success=True, library_path=None, message=None)
def test_evaluate_path_last_lib_present():
# Given
with TemporaryDirectory() as tmpdir:
settings_file = tmpdir + "/test_settings.ini"
settings = QSettings(settings_file, QSettings.Format.IniFormat)
settings.setValue(SettingItems.LAST_LIBRARY, tmpdir)
settings.sync()
makedirs(Path(tmpdir) / TS_FOLDER_NAME)
driver = TestDriver(settings)
# When
result = driver.evaluate_path(None)
# Then
assert result == LibraryStatus(success=True, library_path=Path(tmpdir))

View File

@@ -0,0 +1,51 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from pathlib import Path
from time import time
from src.core.enums import LibraryPrefs
from src.qt.widgets.migration_modal import JsonMigrationModal
CWD = Path(__file__)
def test_json_migration():
modal = JsonMigrationModal(CWD.parent / "fixtures" / "json_library")
modal.migrate(skip_ui=True)
start = time()
while not modal.done and (time() - start < 60):
pass
# Entries ==================================================================
# Count
assert len(modal.json_lib.entries) == modal.sql_lib.entries_count
# Path Parity
assert modal.check_path_parity()
# Field Parity
assert modal.check_field_parity()
# Tags =====================================================================
# Count
assert len(modal.json_lib.tags) == len(modal.sql_lib.tags)
# Name Parity
assert modal.check_name_parity()
# Shorthand Parity
assert modal.check_shorthand_parity()
# Subtag/Parent Tag Parity
assert modal.check_subtag_parity()
# Alias Parity
assert modal.check_alias_parity()
# Color Parity
assert modal.check_color_parity()
# Extension Filter List ====================================================
# Count
assert len(modal.json_lib.ext_list) == len(modal.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST))
# List Type
assert modal.check_ext_type()
# No Leading Dot
for ext in modal.sql_lib.prefs(LibraryPrefs.EXTENSION_LIST):
assert ext[0] != "."

512
tests/test_library.py Normal file
View File

@@ -0,0 +1,512 @@
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from src.core.enums import DefaultEnum, LibraryPrefs
from src.core.library.alchemy import Entry, Library
from src.core.library.alchemy.enums import FilterState
from src.core.library.alchemy.fields import TextField, _FieldID
from src.core.library.alchemy.models import Tag
def test_library_add_alias(library, generate_tag):
tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
parent_ids: set[int] = set()
alias_ids: set[int] = set()
alias_names: set[str] = set()
alias_names.add("test_alias")
library.update_tag(tag, parent_ids, alias_names, alias_ids)
alias_ids = library.get_tag(tag.id).alias_ids
assert len(alias_ids) == 1
def test_library_get_alias(library, generate_tag):
tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
parent_ids: set[int] = set()
alias_ids: set[int] = set()
alias_names: set[str] = set()
alias_names.add("test_alias")
library.update_tag(tag, parent_ids, alias_names, alias_ids)
alias_ids = library.get_tag(tag.id).alias_ids
assert library.get_alias(tag.id, alias_ids[0]).name == "test_alias"
def test_library_update_alias(library, generate_tag):
tag: Tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
parent_ids: set[int] = set()
alias_ids: set[int] = set()
alias_names: set[str] = set()
alias_names.add("test_alias")
library.update_tag(tag, parent_ids, alias_names, alias_ids)
alias_ids = library.get_tag(tag.id).alias_ids
assert library.get_alias(tag.id, alias_ids[0]).name == "test_alias"
alias_names.remove("test_alias")
alias_names.add("alias_update")
library.update_tag(tag, parent_ids, alias_names, alias_ids)
tag = library.get_tag(tag.id)
assert len(tag.alias_ids) == 1
assert library.get_alias(tag.id, tag.alias_ids[0]).name == "alias_update"
@pytest.mark.parametrize("library", [TemporaryDirectory()], indirect=True)
def test_library_add_file(library):
"""Check Entry.path handling for insert vs lookup"""
entry = Entry(
path=Path("bar.txt"),
folder=library.folder,
fields=library.default_fields,
)
assert not library.has_path_entry(entry.path)
assert library.add_entries([entry])
assert library.has_path_entry(entry.path)
def test_create_tag(library, generate_tag):
# tag already exists
assert not library.add_tag(generate_tag("foo", id=1000))
# new tag name
tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
assert tag.id == 123
tag_inc = library.add_tag(generate_tag("yyy"))
assert tag_inc.id > 1000
def test_tag_self_parent(library, generate_tag):
# tag already exists
assert not library.add_tag(generate_tag("foo", id=1000))
# new tag name
tag = library.add_tag(generate_tag("xxx", id=123))
assert tag
assert tag.id == 123
library.update_tag(tag, {tag.id}, {}, {})
tag = library.get_tag(tag.id)
assert len(tag.parent_ids) == 0
def test_library_search(library, generate_tag, entry_full):
assert library.entries_count == 2
tag = list(entry_full.tags)[0]
results = library.search_library(
FilterState.from_tag_name(tag.name),
)
assert results.total_count == 1
assert len(results) == 1
def test_tag_search(library):
tag = library.tags[0]
assert library.search_tags(tag.name.lower())
assert library.search_tags(tag.name.upper())
assert library.search_tags(tag.name[2:-2])
assert library.search_tags(tag.name * 2) == [set(), set()]
def test_get_entry(library: Library, entry_min):
assert entry_min.id
result = library.get_entry_full(entry_min.id)
assert result
assert len(result.tags) == 1
def test_entries_count(library):
entries = [Entry(path=Path(f"{x}.txt"), folder=library.folder, fields=[]) for x in range(10)]
new_ids = library.add_entries(entries)
assert len(new_ids) == 10
results = library.search_library(FilterState.show_all().with_page_size(5))
assert results.total_count == 12
assert len(results) == 5
def test_parents_add(library, generate_tag):
# Given
tag: Tag = library.tags[0]
assert tag.id is not None
parent_tag = generate_tag("parent_tag_01")
parent_tag = library.add_tag(parent_tag)
assert parent_tag.id is not None
# When
assert library.add_parent_tag(tag.id, parent_tag.id)
# Then
assert tag.id is not None
tag = library.get_tag(tag.id)
assert tag.parent_ids
def test_remove_tag(library, generate_tag):
tag = library.add_tag(generate_tag("food", id=123))
assert tag
tag_count = len(library.tags)
library.remove_tag(tag)
assert len(library.tags) == tag_count - 1
@pytest.mark.parametrize("is_exclude", [True, False])
def test_search_filter_extensions(library, is_exclude):
# Given
entries = list(library.get_entries())
assert len(entries) == 2, entries
library.set_prefs(LibraryPrefs.IS_EXCLUDE_LIST, is_exclude)
library.set_prefs(LibraryPrefs.EXTENSION_LIST, ["md"])
# When
results = library.search_library(
FilterState.show_all(),
)
# Then
assert results.total_count == 1
assert len(results) == 1
entry = results[0]
assert (entry.path.suffix == ".txt") == is_exclude
def test_search_library_case_insensitive(library):
# Given
entries = list(library.get_entries(with_joins=True))
assert len(entries) == 2, entries
entry = entries[0]
tag = list(entry.tags)[0]
# When
results = library.search_library(
FilterState.from_tag_name(tag.name.upper()),
)
# Then
assert results.total_count == 1
assert len(results) == 1
assert results[0].id == entry.id
def test_preferences(library):
for pref in LibraryPrefs:
assert library.prefs(pref) == pref.default
def test_remove_entry_field(library, entry_full):
title_field = entry_full.text_fields[0]
library.remove_entry_field(title_field, [entry_full.id])
entry = next(library.get_entries(with_joins=True))
assert not entry.text_fields
def test_remove_field_entry_with_multiple_field(library, entry_full):
# Given
title_field = entry_full.text_fields[0]
# When
# add identical field
assert library.add_field_to_entry(entry_full.id, field_id=title_field.type_key)
# remove entry field
library.remove_entry_field(title_field, [entry_full.id])
# Then one field should remain
entry = next(library.get_entries(with_joins=True))
assert len(entry.text_fields) == 1
def test_update_entry_field(library, entry_full):
title_field = entry_full.text_fields[0]
library.update_entry_field(
entry_full.id,
title_field,
"new value",
)
entry = next(library.get_entries(with_joins=True))
assert entry.text_fields[0].value == "new value"
def test_update_entry_with_multiple_identical_fields(library, entry_full):
# Given
title_field = entry_full.text_fields[0]
# When
# add identical field
library.add_field_to_entry(entry_full.id, field_id=title_field.type_key)
# update one of the fields
library.update_entry_field(
entry_full.id,
title_field,
"new value",
)
# Then only one should be updated
entry = next(library.get_entries(with_joins=True))
assert entry.text_fields[0].value == ""
assert entry.text_fields[1].value == "new value"
def test_mirror_entry_fields(library: Library, entry_full):
# new entry
target_entry = Entry(
folder=library.folder,
path=Path("xxx"),
fields=[
TextField(
type_key=_FieldID.NOTES.name,
value="notes",
position=0,
)
],
)
# insert new entry and get id
entry_id = library.add_entries([target_entry])[0]
# get new entry from library
new_entry = library.get_entry_full(entry_id)
# mirror fields onto new entry
library.mirror_entry_fields(new_entry, entry_full)
# get new entry from library again
entry = library.get_entry_full(entry_id)
# make sure fields are there after getting it from the library again
assert len(entry.fields) == 2
assert {x.type_key for x in entry.fields} == {
_FieldID.TITLE.name,
_FieldID.NOTES.name,
}
def test_merge_entries(library: Library):
a = Entry(
folder=library.folder,
path=Path("a"),
fields=[
TextField(type_key=_FieldID.AUTHOR.name, value="Author McAuthorson", position=0),
TextField(type_key=_FieldID.DESCRIPTION.name, value="test description", position=2),
],
)
b = Entry(
folder=library.folder,
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])
entry_b = library.get_entry_full(ids[1])
tag_0 = library.add_tag(Tag(id=1000, name="tag_0"))
tag_1 = library.add_tag(Tag(id=1001, name="tag_1"))
tag_2 = library.add_tag(Tag(id=1002, name="tag_2"))
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()
def test_remove_tags_from_entries(library, entry_full):
removed_tag_id = -1
for tag in entry_full.tags:
removed_tag_id = tag.id
library.remove_tags_from_entries(entry_full.id, tag.id)
entry = next(library.get_entries(with_joins=True))
assert removed_tag_id not in [t.id for t in entry.tags]
@pytest.mark.parametrize(
["query_name", "has_result"],
[
(1, 1),
("1", 1),
("xxx", 0),
(222, 0),
],
)
def test_search_entry_id(library: Library, query_name: int, has_result):
result = library.get_entry(query_name)
assert (result is not None) == has_result
def test_update_field_order(library, entry_full):
# Given
title_field = entry_full.text_fields[0]
# When add two more fields
library.add_field_to_entry(entry_full.id, field_id=title_field.type_key, value="first")
library.add_field_to_entry(entry_full.id, field_id=title_field.type_key, value="second")
# remove the one on first position
assert title_field.position == 0
library.remove_entry_field(title_field, [entry_full.id])
# recalculate the positions
library.update_field_position(
type(title_field),
title_field.type_key,
entry_full.id,
)
# Then
entry = next(library.get_entries(with_joins=True))
assert entry.text_fields[0].position == 0
assert entry.text_fields[0].value == "first"
assert entry.text_fields[1].position == 1
assert entry.text_fields[1].value == "second"
def test_library_prefs_multiple_identical_vals():
# check the preferences are inherited from DefaultEnum
assert issubclass(LibraryPrefs, DefaultEnum)
# create custom settings with identical values
class TestPrefs(DefaultEnum):
FOO = 1
BAR = 1
assert TestPrefs.FOO.default == 1
assert TestPrefs.BAR.default == 1
assert TestPrefs.BAR.name == "BAR"
# accessing .value should raise exception
with pytest.raises(AttributeError):
assert TestPrefs.BAR.value
def test_path_search_ilike(library: Library):
results = library.search_library(FilterState.from_path("bar.md"))
assert results.total_count == 1
assert len(results.items) == 1
def test_path_search_like(library: Library):
results = library.search_library(FilterState.from_path("BAR.MD"))
assert results.total_count == 0
assert len(results.items) == 0
def test_path_search_default_with_sep(library: Library):
results = library.search_library(FilterState.from_path("one/two"))
assert results.total_count == 1
assert len(results.items) == 1
def test_path_search_glob_after(library: Library):
results = library.search_library(FilterState.from_path("foo*"))
assert results.total_count == 1
assert len(results.items) == 1
def test_path_search_glob_in_front(library: Library):
results = library.search_library(FilterState.from_path("*bar.md"))
assert results.total_count == 1
assert len(results.items) == 1
def test_path_search_glob_both_sides(library: Library):
results = library.search_library(FilterState.from_path("*one/two*"))
assert results.total_count == 1
assert len(results.items) == 1
def test_path_search_ilike_glob_equality(library: Library):
results_ilike = library.search_library(FilterState.from_path("one/two"))
results_glob = library.search_library(FilterState.from_path("*one/two*"))
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("bar.md"))
results_glob = library.search_library(FilterState.from_path("*bar.md*"))
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("bar"))
results_glob = library.search_library(FilterState.from_path("*bar*"))
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("bar.md"))
results_glob = library.search_library(FilterState.from_path("*bar.md*"))
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
def test_path_search_like_glob_equality(library: Library):
results_ilike = library.search_library(FilterState.from_path("ONE/two"))
results_glob = library.search_library(FilterState.from_path("*ONE/two*"))
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("BAR.MD"))
results_glob = library.search_library(FilterState.from_path("*BAR.MD*"))
assert [e.id for e in results_ilike.items] == [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("BAR.MD"))
results_glob = library.search_library(FilterState.from_path("*bar.md*"))
assert [e.id for e in results_ilike.items] != [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
results_ilike = library.search_library(FilterState.from_path("bar.md"))
results_glob = library.search_library(FilterState.from_path("*BAR.MD*"))
assert [e.id for e in results_ilike.items] != [e.id for e in results_glob.items]
results_ilike, results_glob = None, None
@pytest.mark.parametrize(["filetype", "num_of_filetype"], [("md", 1), ("txt", 1), ("png", 0)])
def test_filetype_search(library, filetype, num_of_filetype):
results = library.search_library(FilterState.from_filetype(filetype))
assert len(results.items) == 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, filetype, num_of_filetype):
results = file_mediatypes_library.search_library(FilterState.from_filetype(filetype))
assert len(results.items) == num_of_filetype
@pytest.mark.parametrize(["mediatype", "num_of_mediatype"], [("plaintext", 2), ("image", 0)])
def test_mediatype_search(library, mediatype, num_of_mediatype):
results = library.search_library(FilterState.from_mediatype(mediatype))
assert len(results.items) == num_of_mediatype

138
tests/test_search.py Normal file
View File

@@ -0,0 +1,138 @@
import pytest
from src.core.library.alchemy.enums import FilterState
from src.core.library.alchemy.library import Library
from src.core.query_lang.util import ParsingError
def verify_count(lib: Library, query: str, count: int):
results = lib.search_library(FilterState.from_search_query(query))
assert results.total_count == count
assert len(results.items) == count
@pytest.mark.parametrize(
["query", "count"],
[
("", 31),
("path:*", 31),
("path:*inherit*", 24),
("path:*comp*", 5),
("special:untagged", 2),
("filetype:png", 25),
("filetype:jpg", 6),
("filetype:'jpg'", 6),
("tag_id:1011", 5),
("tag_id:1038", 11),
("doesnt exist", 0),
("archived", 0),
("favorite", 0),
("tag:favorite", 0),
("circle", 11),
("tag:square", 11),
("green", 5),
("orange", 5),
("tag:orange", 5),
],
)
def test_single_constraint(search_library: Library, query: str, count: int):
verify_count(search_library, query, count)
@pytest.mark.parametrize(
["query", "count"],
[
("circle aND square", 5),
("circle square", 5),
("green AND square", 2),
("green square", 2),
("orange AnD square", 2),
("orange square", 2),
("orange and filetype:png", 5),
("square and filetype:jpg", 2),
("orange filetype:png", 5),
("green path:*inherit*", 4),
],
)
def test_and(search_library: Library, query: str, count: int):
verify_count(search_library, query, count)
@pytest.mark.parametrize(
["query", "count"],
[
("square or circle", 17),
("orange or green", 10),
("orange Or circle", 14),
("orange oR square", 14),
("square OR green", 14),
("circle or green", 14),
("green or circle", 14),
("filetype:jpg or tag:orange", 11),
("red or filetype:png", 28),
("filetype:jpg or path:*comp*", 11),
],
)
def test_or(search_library: Library, query: str, count: int):
verify_count(search_library, query, count)
@pytest.mark.parametrize(
["query", "count"],
[
("not unexistant", 31),
("not path:*", 0),
("not not path:*", 31),
("not special:untagged", 29),
("not filetype:png", 6),
("not filetype:jpg", 25),
("not tag_id:1011", 26),
("not tag_id:1038", 20),
("not green", 26),
("tag:favorite", 0),
("not circle", 20),
("not tag:square", 20),
("circle and not square", 6),
("not circle and square", 6),
("special:untagged or not filetype:jpg", 25),
("not square or green", 22),
],
)
def test_not(search_library: Library, query: str, count: int):
verify_count(search_library, query, count)
@pytest.mark.parametrize(
["query", "count"],
[
("(tag_id:1041)", 11),
("(((tag_id:1041)))", 11),
("not (not tag_id:1041)", 11),
("((circle) and (not square))", 6),
("(not ((square) OR (green)))", 17),
("filetype:png and (tag:square or green)", 12),
],
)
def test_parentheses(search_library: Library, query: str, count: int):
verify_count(search_library, query, count)
@pytest.mark.parametrize(
["query", "count"],
[
("ellipse", 17),
("yellow", 15),
("color", 25),
("shape", 24),
("yellow not green", 10),
],
)
def test_parent_tags(search_library: Library, query: str, count: int):
verify_count(search_library, query, count)
@pytest.mark.parametrize(
"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
search_library.search_library(FilterState.from_search_query(invalid_query))

View File

@@ -0,0 +1,50 @@
import string
from pathlib import Path
import pytest
import ujson as json
CWD = Path(__file__).parent
TRANSLATION_DIR = CWD / ".." / "resources" / "translations"
def load_translation(filename: str) -> dict[str, str]:
with open(TRANSLATION_DIR / filename, encoding="utf-8") as f:
return json.load(f)
def get_translation_filenames() -> list[tuple[str]]:
return [(a.name,) for a in TRANSLATION_DIR.glob("*.json")]
def find_format_keys(format_string: str) -> set[str]:
formatter = string.Formatter()
return set([field[1] for field in formatter.parse(format_string) if field[1] is not None])
@pytest.mark.parametrize(["translation_filename"], get_translation_filenames())
def test_format_key_validity(translation_filename: str):
default_translation = load_translation("en.json")
translation = load_translation(translation_filename)
for key in default_translation:
if key not in translation:
continue
default_keys = find_format_keys(default_translation[key])
translation_keys = find_format_keys(translation[key])
assert default_keys.issuperset(
translation_keys
), f"Translation {translation_filename} for key {key} is using an invalid format key ({translation_keys.difference(default_keys)})" # noqa: E501
assert translation_keys.issuperset(
default_keys
), f"Translation {translation_filename} for key {key} is missing format keys ({default_keys.difference(translation_keys)})" # noqa: E501
@pytest.mark.parametrize(["translation_filename"], get_translation_filenames())
def test_for_unnecessary_translations(translation_filename: str):
default_translation = load_translation("en.json")
translation = load_translation(translation_filename)
assert set(
default_translation.keys()
).issuperset(
translation.keys()
), f"Translation {translation_filename} has unnecessary keys ({set(translation.keys()).difference(default_translation.keys())})" # noqa: E501