refactor!: change layout; renaming, import and build change incoming
166
tests/conftest.py
Normal 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
|
||||
BIN
tests/fixtures/empty_libraries/DB_VERSION_6/.TagStudio/ts_library.sqlite
vendored
Normal file
BIN
tests/fixtures/empty_libraries/DB_VERSION_7/.TagStudio/ts_library.sqlite
vendored
Normal file
BIN
tests/fixtures/empty_libraries/DB_VERSION_8/.TagStudio/ts_library.sqlite
vendored
Normal file
1
tests/fixtures/json_library/.TagStudio/ts_library.json
vendored
Normal file
10
tests/fixtures/result.dupeguru
vendored
Normal 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
BIN
tests/fixtures/sample.ods
vendored
Normal file
BIN
tests/fixtures/sample.odt
vendored
Normal file
BIN
tests/fixtures/sample.pdf
vendored
Normal file
8
tests/fixtures/sample.svg
vendored
Normal 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 |
431
tests/fixtures/search_library/.TagStudio/ts_library.json
vendored
Normal 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": "." }
|
||||
]
|
||||
}
|
||||
BIN
tests/fixtures/search_library/.TagStudio/ts_library.sqlite
vendored
Normal file
BIN
tests/fixtures/search_library/comp colors shapes/r_circle_b_square.png
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
tests/fixtures/search_library/comp colors shapes/r_circle_g_square.png
vendored
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
tests/fixtures/search_library/comp colors shapes/r_circle_o_square.png
vendored
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
tests/fixtures/search_library/comp colors shapes/r_circle_r_square.png
vendored
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
tests/fixtures/search_library/comp colors shapes/r_circle_y_square.png
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/blue.jpg
vendored
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/blue_circle.jpg
vendored
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/blue_ellipse.png
vendored
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/blue_square.jpg
vendored
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/circle.png
vendored
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/ellipse.png
vendored
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/green.png
vendored
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/green_circle.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/green_ellipse.png
vendored
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/green_square.png
vendored
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/orange.png
vendored
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/orange_circle.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/orange_ellipse.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/orange_square.png
vendored
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/red.jpg
vendored
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/red_circle.jpg
vendored
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/red_ellipse.png
vendored
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/red_square.jpg
vendored
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/shape.png
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/square.png
vendored
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/yellow.png
vendored
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/yellow_circle.png
vendored
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/yellow_ellipse.png
vendored
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
tests/fixtures/search_library/inherit colors shapes/yellow_square.png
vendored
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
10
tests/fixtures/sidecar_newgrounds.json
vendored
Normal 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"
|
||||
}
|
||||
36
tests/macros/test_dupe_entries.py
Normal 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"),
|
||||
]
|
||||
7
tests/macros/test_folders_tags.py
Normal 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"}
|
||||
31
tests/macros/test_missing_files.py
Normal 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")
|
||||
25
tests/macros/test_refresh_dir.py
Normal 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")]
|
||||
37
tests/macros/test_sidecar.py
Normal 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}
|
||||
4
tests/qt/__snapshots__/test_folders_to_tags.ambr
Normal 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)
|
||||
# ---
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
159
tests/qt/test_build_tag_panel.py
Normal 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"]
|
||||
172
tests/qt/test_field_containers.py
Normal 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>"
|
||||
17
tests/qt/test_flow_widget.py
Normal 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)
|
||||
7
tests/qt/test_folders_to_tags.py
Normal 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
|
||||
18
tests/qt/test_item_thumb.py
Normal 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
|
||||
39
tests/qt/test_preview_panel.py
Normal 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
@@ -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)
|
||||
24
tests/qt/test_tag_panel.py
Normal 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}
|
||||
11
tests/qt/test_tag_search_panel.py
Normal 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()
|
||||
48
tests/qt/test_thumb_renderer.py
Normal 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)
|
||||
47
tests/test_db_migrations.py
Normal 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
@@ -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))
|
||||
51
tests/test_json_migration.py
Normal 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
@@ -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
@@ -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))
|
||||
50
tests/test_translations.py
Normal 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
|
||||