|
14 | 14 |
|
15 | 15 | """Various tests for querying the library database."""
|
16 | 16 |
|
17 |
| -import os |
18 | 17 | import sys
|
19 |
| -import unittest |
20 |
| -from contextlib import contextmanager |
21 |
| -from functools import partial |
| 18 | +from pathlib import Path |
22 | 19 |
|
23 | 20 | import pytest
|
24 | 21 | from mock import patch
|
25 | 22 |
|
26 |
| -import beets.library |
27 |
| -from beets import dbcore, util |
| 23 | +from beets import dbcore |
28 | 24 | from beets.dbcore import types
|
29 | 25 | from beets.dbcore.query import (
|
30 | 26 | InvalidQueryArgumentValueError,
|
31 | 27 | NoneQuery,
|
32 | 28 | ParsingError,
|
| 29 | + PathQuery, |
33 | 30 | )
|
34 | 31 | from beets.test import _common
|
35 |
| -from beets.test.helper import BeetsTestCase, ItemInDBTestCase |
36 |
| -from beets.util import syspath |
| 32 | +from beets.test.helper import BeetsTestCase, TestHelper |
37 | 33 |
|
38 | 34 | # Because the absolute path begins with something like C:, we
|
39 | 35 | # can't disambiguate it from an ordinary query.
|
@@ -442,244 +438,6 @@ def test_eq(self):
|
442 | 438 | assert q3 != q4
|
443 | 439 |
|
444 | 440 |
|
445 |
| -class PathQueryTest(ItemInDBTestCase, AssertsMixin): |
446 |
| - def setUp(self): |
447 |
| - super().setUp() |
448 |
| - |
449 |
| - # This is the item we'll try to match. |
450 |
| - self.i.path = util.normpath("/a/b/c.mp3") |
451 |
| - self.i.title = "path item" |
452 |
| - self.i.album = "path album" |
453 |
| - self.i.store() |
454 |
| - self.lib.add_album([self.i]) |
455 |
| - |
456 |
| - # A second item for testing exclusion. |
457 |
| - i2 = _common.item() |
458 |
| - i2.path = util.normpath("/x/y/z.mp3") |
459 |
| - i2.title = "another item" |
460 |
| - i2.album = "another album" |
461 |
| - self.lib.add(i2) |
462 |
| - self.lib.add_album([i2]) |
463 |
| - |
464 |
| - @contextmanager |
465 |
| - def force_implicit_query_detection(self): |
466 |
| - # Unadorned path queries with path separators in them are considered |
467 |
| - # path queries only when the path in question actually exists. So we |
468 |
| - # mock the existence check to return true. |
469 |
| - beets.dbcore.query.PathQuery.force_implicit_query_detection = True |
470 |
| - yield |
471 |
| - beets.dbcore.query.PathQuery.force_implicit_query_detection = False |
472 |
| - |
473 |
| - def test_path_exact_match(self): |
474 |
| - q = "path:/a/b/c.mp3" |
475 |
| - results = self.lib.items(q) |
476 |
| - self.assert_items_matched(results, ["path item"]) |
477 |
| - |
478 |
| - results = self.lib.albums(q) |
479 |
| - self.assert_albums_matched(results, ["path album"]) |
480 |
| - |
481 |
| - # FIXME: fails on windows |
482 |
| - @unittest.skipIf(sys.platform == "win32", "win32") |
483 |
| - def test_parent_directory_no_slash(self): |
484 |
| - q = "path:/a" |
485 |
| - results = self.lib.items(q) |
486 |
| - self.assert_items_matched(results, ["path item"]) |
487 |
| - |
488 |
| - results = self.lib.albums(q) |
489 |
| - self.assert_albums_matched(results, ["path album"]) |
490 |
| - |
491 |
| - # FIXME: fails on windows |
492 |
| - @unittest.skipIf(sys.platform == "win32", "win32") |
493 |
| - def test_parent_directory_with_slash(self): |
494 |
| - q = "path:/a/" |
495 |
| - results = self.lib.items(q) |
496 |
| - self.assert_items_matched(results, ["path item"]) |
497 |
| - |
498 |
| - results = self.lib.albums(q) |
499 |
| - self.assert_albums_matched(results, ["path album"]) |
500 |
| - |
501 |
| - def test_no_match(self): |
502 |
| - q = "path:/xyzzy/" |
503 |
| - results = self.lib.items(q) |
504 |
| - self.assert_items_matched(results, []) |
505 |
| - |
506 |
| - results = self.lib.albums(q) |
507 |
| - self.assert_albums_matched(results, []) |
508 |
| - |
509 |
| - def test_fragment_no_match(self): |
510 |
| - q = "path:/b/" |
511 |
| - results = self.lib.items(q) |
512 |
| - self.assert_items_matched(results, []) |
513 |
| - |
514 |
| - results = self.lib.albums(q) |
515 |
| - self.assert_albums_matched(results, []) |
516 |
| - |
517 |
| - def test_nonnorm_path(self): |
518 |
| - q = "path:/x/../a/b" |
519 |
| - results = self.lib.items(q) |
520 |
| - self.assert_items_matched(results, ["path item"]) |
521 |
| - |
522 |
| - results = self.lib.albums(q) |
523 |
| - self.assert_albums_matched(results, ["path album"]) |
524 |
| - |
525 |
| - @unittest.skipIf(sys.platform == "win32", WIN32_NO_IMPLICIT_PATHS) |
526 |
| - def test_slashed_query_matches_path(self): |
527 |
| - with self.force_implicit_query_detection(): |
528 |
| - q = "/a/b" |
529 |
| - results = self.lib.items(q) |
530 |
| - self.assert_items_matched(results, ["path item"]) |
531 |
| - |
532 |
| - results = self.lib.albums(q) |
533 |
| - self.assert_albums_matched(results, ["path album"]) |
534 |
| - |
535 |
| - @unittest.skipIf(sys.platform == "win32", WIN32_NO_IMPLICIT_PATHS) |
536 |
| - def test_path_query_in_or_query(self): |
537 |
| - with self.force_implicit_query_detection(): |
538 |
| - q = "/a/b , /a/b" |
539 |
| - results = self.lib.items(q) |
540 |
| - self.assert_items_matched(results, ["path item"]) |
541 |
| - |
542 |
| - def test_non_slashed_does_not_match_path(self): |
543 |
| - with self.force_implicit_query_detection(): |
544 |
| - q = "c.mp3" |
545 |
| - results = self.lib.items(q) |
546 |
| - self.assert_items_matched(results, []) |
547 |
| - |
548 |
| - results = self.lib.albums(q) |
549 |
| - self.assert_albums_matched(results, []) |
550 |
| - |
551 |
| - def test_slashes_in_explicit_field_does_not_match_path(self): |
552 |
| - with self.force_implicit_query_detection(): |
553 |
| - q = "title:/a/b" |
554 |
| - results = self.lib.items(q) |
555 |
| - self.assert_items_matched(results, []) |
556 |
| - |
557 |
| - def test_path_item_regex(self): |
558 |
| - q = "path::c\\.mp3$" |
559 |
| - results = self.lib.items(q) |
560 |
| - self.assert_items_matched(results, ["path item"]) |
561 |
| - |
562 |
| - results = self.lib.albums(q) |
563 |
| - self.assert_albums_matched(results, ["path album"]) |
564 |
| - |
565 |
| - def test_path_album_regex(self): |
566 |
| - q = "path::b" |
567 |
| - results = self.lib.albums(q) |
568 |
| - self.assert_albums_matched(results, ["path album"]) |
569 |
| - |
570 |
| - def test_escape_underscore(self): |
571 |
| - self.add_album( |
572 |
| - path=b"/a/_/title.mp3", |
573 |
| - title="with underscore", |
574 |
| - album="album with underscore", |
575 |
| - ) |
576 |
| - q = "path:/a/_" |
577 |
| - results = self.lib.items(q) |
578 |
| - self.assert_items_matched(results, ["with underscore"]) |
579 |
| - |
580 |
| - results = self.lib.albums(q) |
581 |
| - self.assert_albums_matched(results, ["album with underscore"]) |
582 |
| - |
583 |
| - def test_escape_percent(self): |
584 |
| - self.add_album( |
585 |
| - path=b"/a/%/title.mp3", |
586 |
| - title="with percent", |
587 |
| - album="album with percent", |
588 |
| - ) |
589 |
| - q = "path:/a/%" |
590 |
| - results = self.lib.items(q) |
591 |
| - self.assert_items_matched(results, ["with percent"]) |
592 |
| - |
593 |
| - results = self.lib.albums(q) |
594 |
| - self.assert_albums_matched(results, ["album with percent"]) |
595 |
| - |
596 |
| - def test_escape_backslash(self): |
597 |
| - self.add_album( |
598 |
| - path=rb"/a/\x/title.mp3", |
599 |
| - title="with backslash", |
600 |
| - album="album with backslash", |
601 |
| - ) |
602 |
| - q = "path:/a/\\\\x" |
603 |
| - results = self.lib.items(q) |
604 |
| - self.assert_items_matched(results, ["with backslash"]) |
605 |
| - |
606 |
| - results = self.lib.albums(q) |
607 |
| - self.assert_albums_matched(results, ["album with backslash"]) |
608 |
| - |
609 |
| - def test_case_sensitivity(self): |
610 |
| - self.add_album(path=b"/A/B/C2.mp3", title="caps path") |
611 |
| - |
612 |
| - makeq = partial(beets.dbcore.query.PathQuery, "path", "/A/B") |
613 |
| - |
614 |
| - results = self.lib.items(makeq(case_sensitive=True)) |
615 |
| - self.assert_items_matched(results, ["caps path"]) |
616 |
| - |
617 |
| - results = self.lib.items(makeq(case_sensitive=False)) |
618 |
| - self.assert_items_matched(results, ["path item", "caps path"]) |
619 |
| - |
620 |
| - # FIXME: Also create a variant of this test for windows, which tests |
621 |
| - # both os.sep and os.altsep |
622 |
| - @unittest.skipIf(sys.platform == "win32", "win32") |
623 |
| - def test_path_sep_detection(self): |
624 |
| - is_path_query = beets.dbcore.query.PathQuery.is_path_query |
625 |
| - |
626 |
| - with self.force_implicit_query_detection(): |
627 |
| - assert is_path_query("/foo/bar") |
628 |
| - assert is_path_query("foo/bar") |
629 |
| - assert is_path_query("foo/") |
630 |
| - assert not is_path_query("foo") |
631 |
| - assert is_path_query("foo/:bar") |
632 |
| - assert not is_path_query("foo:bar/") |
633 |
| - assert not is_path_query("foo:/bar") |
634 |
| - |
635 |
| - # FIXME: shouldn't this also work on windows? |
636 |
| - @unittest.skipIf(sys.platform == "win32", WIN32_NO_IMPLICIT_PATHS) |
637 |
| - def test_detect_absolute_path(self): |
638 |
| - """Test detection of implicit path queries based on whether or |
639 |
| - not the path actually exists, when using an absolute path query. |
640 |
| -
|
641 |
| - Thus, don't use the `force_implicit_query_detection()` |
642 |
| - contextmanager which would disable the existence check. |
643 |
| - """ |
644 |
| - is_path_query = beets.dbcore.query.PathQuery.is_path_query |
645 |
| - |
646 |
| - path = self.touch(os.path.join(b"foo", b"bar")) |
647 |
| - assert os.path.isabs(util.syspath(path)) |
648 |
| - path_str = path.decode("utf-8") |
649 |
| - |
650 |
| - # The file itself. |
651 |
| - assert is_path_query(path_str) |
652 |
| - |
653 |
| - # The parent directory. |
654 |
| - parent = os.path.dirname(path_str) |
655 |
| - assert is_path_query(parent) |
656 |
| - |
657 |
| - # Some non-existent path. |
658 |
| - assert not is_path_query(f"{path_str}baz") |
659 |
| - |
660 |
| - def test_detect_relative_path(self): |
661 |
| - """Test detection of implicit path queries based on whether or |
662 |
| - not the path actually exists, when using a relative path query. |
663 |
| -
|
664 |
| - Thus, don't use the `force_implicit_query_detection()` |
665 |
| - contextmanager which would disable the existence check. |
666 |
| - """ |
667 |
| - is_path_query = beets.dbcore.query.PathQuery.is_path_query |
668 |
| - |
669 |
| - self.touch(os.path.join(b"foo", b"bar")) |
670 |
| - |
671 |
| - # Temporarily change directory so relative paths work. |
672 |
| - cur_dir = os.getcwd() |
673 |
| - try: |
674 |
| - os.chdir(syspath(self.temp_dir)) |
675 |
| - assert is_path_query("foo/") |
676 |
| - assert is_path_query("foo/bar") |
677 |
| - assert is_path_query("foo/bar:tagada") |
678 |
| - assert not is_path_query("bar") |
679 |
| - finally: |
680 |
| - os.chdir(cur_dir) |
681 |
| - |
682 |
| - |
683 | 441 | class IntQueryTest(BeetsTestCase):
|
684 | 442 | def test_exact_value_match(self):
|
685 | 443 | item = self.add_item(bpm=120)
|
@@ -1104,3 +862,104 @@ def test_filter_items_by_common_field(self):
|
1104 | 862 | q = "artpath::A Album1"
|
1105 | 863 | results = self.lib.items(q)
|
1106 | 864 | self.assert_items_matched(results, ["Album1 Item1", "Album1 Item2"])
|
| 865 | + |
| 866 | + |
| 867 | +@pytest.fixture(scope="class") |
| 868 | +def helper(): |
| 869 | + helper = TestHelper() |
| 870 | + helper.setup_beets() |
| 871 | + |
| 872 | + yield helper |
| 873 | + |
| 874 | + helper.teardown_beets() |
| 875 | + |
| 876 | + |
| 877 | +class TestPathQuery: |
| 878 | + _p = pytest.param |
| 879 | + |
| 880 | + @pytest.fixture(scope="class") |
| 881 | + def lib(self, helper): |
| 882 | + helper.add_item(path="/a/b/c.mp3", title="path item") |
| 883 | + helper.add_item(path="/x/y/z.mp3", title="another item") |
| 884 | + helper.add_item(path=b"/c/_/title.mp3", title="with underscore") |
| 885 | + helper.add_item(path=b"/c/%/title.mp3", title="with percent") |
| 886 | + helper.add_item(path=rb"/c/\x/title.mp3", title="with backslash") |
| 887 | + helper.add_item(path=b"/A/B/C2.mp3", title="caps path") |
| 888 | + |
| 889 | + return helper.lib |
| 890 | + |
| 891 | + @pytest.mark.parametrize( |
| 892 | + "q, expected_titles", |
| 893 | + [ |
| 894 | + _p("path:/a/b/c.mp3", ["path item"], id="exact-match"), |
| 895 | + _p("path:/a", ["path item"], id="parent-dir-no-slash"), |
| 896 | + _p("path:/a/", ["path item"], id="parent-dir-with-slash"), |
| 897 | + _p("path:/xyzzy/", [], id="no-match"), |
| 898 | + _p("path:/b/", [], id="fragment-no-match"), |
| 899 | + _p("path:/x/../a/b", ["path item"], id="non-normalized"), |
| 900 | + _p("path::c\\.mp3$", ["path item"], id="regex"), |
| 901 | + _p("path:/c/_", ["with underscore"], id="underscore-escaped"), |
| 902 | + _p("path:/c/%", ["with percent"], id="percent-escaped"), |
| 903 | + _p("path:/c/\\\\x", ["with backslash"], id="backslash-escaped"), |
| 904 | + ], |
| 905 | + ) |
| 906 | + def test_explicit(self, lib, q, expected_titles): |
| 907 | + assert {i.title for i in lib.items(q)} == set(expected_titles) |
| 908 | + |
| 909 | + @pytest.mark.skipif(sys.platform == "win32", reason=WIN32_NO_IMPLICIT_PATHS) |
| 910 | + @pytest.mark.parametrize( |
| 911 | + "q, expected_titles", |
| 912 | + [ |
| 913 | + _p("/a/b", ["path item"], id="slashed-query"), |
| 914 | + _p("/a/b , /a/b", ["path item"], id="path-in-or-query"), |
| 915 | + _p("c.mp3", [], id="no-slash-no-match"), |
| 916 | + _p("title:/a/b", [], id="slash-with-explicit-field-no-match"), |
| 917 | + ], |
| 918 | + ) |
| 919 | + def test_implicit(self, monkeypatch, lib, q, expected_titles): |
| 920 | + monkeypatch.setattr( |
| 921 | + "beets.dbcore.query.PathQuery.is_path_query", lambda path: True |
| 922 | + ) |
| 923 | + |
| 924 | + assert {i.title for i in lib.items(q)} == set(expected_titles) |
| 925 | + |
| 926 | + @pytest.mark.parametrize( |
| 927 | + "case_sensitive, expected_titles", |
| 928 | + [ |
| 929 | + _p(True, [], id="non-caps-dont-match-caps"), |
| 930 | + _p(False, ["caps path"], id="non-caps-match-caps"), |
| 931 | + ], |
| 932 | + ) |
| 933 | + def test_case_sensitivity( |
| 934 | + self, lib, monkeypatch, case_sensitive, expected_titles |
| 935 | + ): |
| 936 | + q = "path:/a/b/c2.mp3" |
| 937 | + monkeypatch.setattr( |
| 938 | + "beets.util.case_sensitive", lambda *_: case_sensitive |
| 939 | + ) |
| 940 | + |
| 941 | + assert {i.title for i in lib.items(q)} == set(expected_titles) |
| 942 | + |
| 943 | + # FIXME: Also create a variant of this test for windows, which tests |
| 944 | + # both os.sep and os.altsep |
| 945 | + @pytest.mark.skipif(sys.platform == "win32", reason=WIN32_NO_IMPLICIT_PATHS) |
| 946 | + @pytest.mark.parametrize( |
| 947 | + "q, is_path_query", |
| 948 | + [ |
| 949 | + ("/foo/bar", True), |
| 950 | + ("foo/bar", True), |
| 951 | + ("foo/", True), |
| 952 | + ("foo", False), |
| 953 | + ("foo/:bar", True), |
| 954 | + ("foo:bar/", False), |
| 955 | + ("foo:/bar", False), |
| 956 | + ], |
| 957 | + ) |
| 958 | + def test_path_sep_detection(self, monkeypatch, tmp_path, q, is_path_query): |
| 959 | + monkeypatch.chdir(tmp_path) |
| 960 | + (tmp_path / "foo").mkdir() |
| 961 | + (tmp_path / "foo" / "bar").touch() |
| 962 | + if Path(q).is_absolute(): |
| 963 | + q = str(tmp_path / q[1:]) |
| 964 | + |
| 965 | + assert PathQuery.is_path_query(q) == is_path_query |
0 commit comments